Files
shanxia2.0/web-admin/src/views/db/inventory/index.vue
T
2026-08-04 14:02:01 +08:00

1170 lines
39 KiB
Vue

<template>
<div class="page-container">
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">地磅出入库</h2>
</div>
<div class="filter-block">
<el-form :model="filters" class="filter-form" @submit.prevent>
<div class="filter-grid">
<el-form-item label="日期范围">
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
style="width: 100%"
@change="handleDateRangeChange"
/>
</el-form-item>
<el-form-item label="出入库单号">
<el-input v-model="filters.batchNo" placeholder="出入库单号" clearable @keyup.enter="handleSearch" />
</el-form-item>
<el-form-item label="业务类型">
<el-select v-model="filters.businessType" placeholder="全部" clearable @change="handleSearch">
<el-option v-for="item in businessTypeOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="filters.status" placeholder="全部" clearable @change="handleSearch">
<el-option v-for="item in statusOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
<el-form-item label="车牌号">
<el-input v-model="filters.vehicleNo" placeholder="车牌号" clearable @keyup.enter="handleSearch" />
</el-form-item>
</div>
<div class="filter-actions">
<div class="filter-actions-start" />
<div class="filter-actions-end">
<el-button class="btn-ghost" :loading="loading" @click="handleResetFilters">
<el-icon><RefreshLeft /></el-icon>
重置
</el-button>
<el-button class="btn-main" type="primary" :loading="loading" @click="handleSearch">
<el-icon><Search /></el-icon>
搜索
</el-button>
</div>
</div>
</el-form>
</div>
<div class="list-toolbar">
<div class="list-toolbar-row db-list-toolbar-row">
<ListToolbarDateShortcuts
variant="select"
: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>
<el-button
class="btn-ghost"
:disabled="selectedRows.length !== 1"
@click="handleToolbarEdit"
>
<el-icon><Edit /></el-icon>
编辑
</el-button>
<el-button
class="btn-ghost"
:disabled="selectedRows.length !== 1 || !!selectedRows[0]?.hasWeighingSlip"
@click="handleGenerateWeighing(selectedRows[0])"
>
<el-icon><Plus /></el-icon>
{{
selectedRows.length === 1 && selectedRows[0]?.hasWeighingSlip
? '已生成地磅单'
: '生成地磅单'
}}
</el-button>
<el-button
class="btn-ghost"
:disabled="selectedRows.length !== 1"
@click="handleCopyFillLink(selectedRows[0])"
>
<el-icon><Link /></el-icon>
磅单二维码
</el-button>
<el-button
class="btn-ghost"
type="danger"
:disabled="
selectedRows.length !== 1 ||
!canDeleteRecord(selectedRows[0]) ||
!canDeleteByCreator(selectedRows[0])
"
:title="
selectedRows.length === 1 ? getDeleteRecordDisabledTitle(selectedRows[0]) : ''
"
@click="handleToolbarDelete"
>
<el-icon><Delete /></el-icon>
删除
</el-button>
<el-button class="btn-ghost" :loading="exportLoading" @click="handleExport">
<el-icon><Download /></el-icon>
导出 Excel
</el-button>
<ListToolbarEndActions
:loading="loading"
:column-defs="listColumnDefs"
v-model:visible-columns="visibleColumns"
v-model:check-all="checkAll"
:is-indeterminate="isIndeterminate"
@refresh="loadData"
@reset-columns="resetToDefault"
/>
</div>
</div>
<div class="list-body list-wrap">
<ListTable table-key="db-inventory"
class="list-table"
:data="tableData"
v-loading="loading"
stripe
row-key="id"
show-summary
:summary-method="getSummary"
@sort-change="handleSortChange"
@selection-change="handleSelectionChange"
@row-dblclick="handleView"
>
<el-table-column type="selection" min-width="55" align="center" />
<el-table-column type="index" label="序号" min-width="60" align="center" header-align="center" />
<el-table-column
v-if="isColumnVisible('id')"
prop="id"
label="ID"
min-width="100"
align="center"
header-align="center"
show-overflow-tooltip
sortable="custom"
:sort-order="getSortOrder('id')"
/>
<el-table-column
v-if="isColumnVisible('planNo')"
prop="planNo"
label="计划号"
min-width="140"
show-overflow-tooltip
sortable="custom"
:sort-order="getSortOrder('planNo')"
/>
<el-table-column
prop="batchNo"
label="出入库单号"
min-width="140"
show-overflow-tooltip
sortable="custom"
:sort-order="getSortOrder('batchNo')"
>
<template #default="{ row }">
<el-link type="primary" @click="handleView(row)">{{ row.batchNo }}</el-link>
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('date')"
prop="date"
label="日期"
min-width="120"
sortable="custom"
:sort-order="getSortOrder('date')"
/>
<el-table-column
v-if="isColumnVisible('partnerCompany')"
prop="partnerCompany"
label="往来单位"
min-width="240"
align="left"
header-align="left"
class-name="col-name-wrap"
label-class-name="col-name-wrap"
sortable="custom"
:sort-order="getSortOrder('partnerCompany')"
/>
<el-table-column
v-if="isColumnVisible('businessType')"
prop="businessType"
label="业务类型"
min-width="100"
sortable="custom"
:sort-order="getSortOrder('businessType')"
>
<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('status')"
prop="status"
label="状态"
min-width="110"
sortable="custom"
:sort-order="getSortOrder('status')"
>
<template #default="{ row }">
<el-tag :type="getStatusTagType(row.status)" size="small">
{{ row.status || '待处理' }}
</el-tag>
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('product')"
prop="product"
label="产品"
min-width="240"
align="left"
header-align="left"
class-name="col-name-wrap"
label-class-name="col-name-wrap"
sortable="custom"
:sort-order="getSortOrder('product')"
/>
<el-table-column
v-if="isColumnVisible('specification')"
prop="specification"
label="规格"
min-width="120"
show-overflow-tooltip
sortable="custom"
:sort-order="getSortOrder('specification')"
/>
<el-table-column
v-if="isColumnVisible('vehicleNo')"
prop="vehicleNo"
label="车牌号"
min-width="120"
sortable="custom"
:sort-order="getSortOrder('vehicleNo')"
/>
<el-table-column
v-if="isColumnVisible('quantity')"
prop="quantity"
label="数量(吨)"
min-width="110"
sortable="custom"
:sort-order="getSortOrder('quantity')"
/>
<el-table-column
v-if="isColumnVisible('netWeight')"
prop="netWeight"
label="净重(吨)"
min-width="110"
sortable="custom"
:sort-order="getSortOrder('netWeight')"
>
<template #default="{ row }">
{{ formatKgToTon(row.netWeight) }}
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('grossWeight')"
prop="grossWeight"
label="毛重(吨)"
min-width="110"
sortable="custom"
:sort-order="getSortOrder('grossWeight')"
>
<template #default="{ row }">
{{ formatKgToTon(row.grossWeight) }}
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('grossWeightTime')"
prop="grossWeightTime"
label="毛重时间"
min-width="160"
sortable="custom"
:sort-order="getSortOrder('grossWeightTime')"
>
<template #default="{ row }">
{{ row.grossWeightTime || '--' }}
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('tareWeight')"
prop="tareWeight"
label="皮重(吨)"
min-width="110"
sortable="custom"
:sort-order="getSortOrder('tareWeight')"
>
<template #default="{ row }">
{{ formatKgToTon(row.tareWeight) }}
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('tareWeightTime')"
prop="tareWeightTime"
label="皮重时间"
min-width="160"
sortable="custom"
:sort-order="getSortOrder('tareWeightTime')"
>
<template #default="{ row }">
{{ row.tareWeightTime || '--' }}
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('deduction')"
prop="deduction"
label="扣杂(吨)"
min-width="110"
sortable="custom"
:sort-order="getSortOrder('deduction')"
>
<template #default="{ row }">
{{ formatKgToTon(row.deduction) }}
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('remarks')"
prop="remarks"
label="备注"
min-width="150"
show-overflow-tooltip
sortable="custom"
:sort-order="getSortOrder('remarks')"
/>
<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 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, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="handlePageChange"
@size-change="handleSizeChange"
/>
</div>
</div>
</section>
<el-dialog
v-model="modalOpen"
:title="editingRecord ? '修改记录' : '新增记录'"
width="740px"
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="date">
<el-date-picker
v-model="formData.date"
type="date"
placeholder="选择日期"
style="width: 100%"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
/>
</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="partnerCompany">
<DbCrmCustomerSelect
v-model="formData.partnerCompany"
:allow-create="false"
placeholder="请从客户管理中搜索选择"
/>
</el-form-item>
<el-form-item label="产品" prop="productId">
<DbCrmProductSelect
v-model:product-id="formData.productId"
v-model:product-name="formData.product"
v-model:specification="formData.specification"
placeholder="请搜索并选择上架产品"
/>
</el-form-item>
<el-form-item label="规格">
<el-input
v-model="formData.specification"
readonly
placeholder="选择产品后自动带出"
/>
</el-form-item>
<el-form-item label="数量(吨)" prop="quantity" class="db-form-quantity-red">
<el-input-number
v-model="formData.quantity"
:min="0"
:precision="2"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="车牌号" prop="vehicleNo">
<PlVehicleSelect v-model="formData.vehicleNo" placeholder="请选择三方或自有车辆车牌" />
</el-form-item>
<el-form-item label="计划号" prop="planNo" class="form-col-span-2">
<el-input v-model="formData.planNo" placeholder="请输入计划号" :readonly="isPlanNoReadonly" />
</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" @click="handleSubmit">
<el-icon><Check /></el-icon>
确定
</el-button>
</template>
</el-dialog>
<DbInventoryRelatedViewDialog
v-model:visible="relatedModalOpen"
:loading="loadingRelated"
:data="relatedData"
>
<template v-if="viewingRecord" #hero-actions>
<el-button class="btn-main" type="primary" @click="handleEdit(viewingRecord)">
<el-icon><Edit /></el-icon>
编辑
</el-button>
<el-button
class="btn-ghost"
:disabled="viewingRecord.hasWeighingSlip"
@click="handleGenerateWeighing(viewingRecord)"
>
<el-icon><Plus /></el-icon>
{{ viewingRecord.hasWeighingSlip ? '已生成地磅单' : '生成地磅单' }}
</el-button>
<el-button class="btn-ghost" @click="handleCopyFillLink(viewingRecord)">
<el-icon><Link /></el-icon>
磅单二维码
</el-button>
<el-button
class="btn-ghost"
type="danger"
:disabled="!canDeleteRecord(viewingRecord) || !canDeleteByCreator(viewingRecord)"
:title="viewingRecord ? getDeleteRecordDisabledTitle(viewingRecord) : ''"
@click="handleViewDelete"
>
<el-icon><Delete /></el-icon>
删除
</el-button>
</template>
</DbInventoryRelatedViewDialog>
<DbPoundSlipQrcodeDialog v-model:visible="qrcodeModalOpen" :inventory-id="qrcodeInventoryId" />
</div>
</template>
<script setup lang="ts">
/**
* UI 模式 P02 · 复杂业务列表
* 参考:/#/crm/quote · src/views/crm/quote/index.vue
*/
import type { TableColumnCtx } from 'element-plus'
import {
Check,
Close,
CopyDocument,
Delete,
Download,
Edit,
Link,
Plus,
RefreshLeft,
Search,
} 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 ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import { canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
import { syncViewRowFromFlatList } from '@/utils/view-dialog-refresh'
import DbCrmProductSelect from '@/components/business/db/DbCrmProductSelect.vue'
import DbInventoryRelatedViewDialog from '@/components/business/db/DbInventoryRelatedViewDialog.vue'
import DbPoundSlipQrcodeDialog from '@/components/business/db/DbPoundSlipQrcodeDialog.vue'
import PlVehicleSelect from '@/components/business/pl/PlVehicleSelect.vue'
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
import { $modal } from '@/utils/modal'
import {
inventoryApi,
type InventoryCreate,
type InventoryListParams,
type InventoryRecord,
type InventoryRelatedData
} from '@/api/db/inventory'
defineOptions({ name: 'DbInventory' })
const route = useRoute()
const router = useRouter()
const loading = ref(false)
const listColumnDefs = [
{ prop: 'id', label: 'ID', defaultVisible: false },
{ prop: 'planNo', label: '计划号' },
{ prop: 'date', label: '日期' },
{ prop: 'partnerCompany', label: '往来单位' },
{ prop: 'businessType', label: '业务类型' },
{ prop: 'status', label: '状态' },
{ prop: 'product', label: '产品' },
{ prop: 'specification', label: '规格' },
{ prop: 'vehicleNo', label: '车牌号' },
{ prop: 'quantity', label: '数量(吨)' },
{ prop: 'netWeight', label: '净重(吨)' },
{ prop: 'grossWeight', label: '毛重(吨)' },
{ prop: 'grossWeightTime', label: '毛重时间' },
{ prop: 'tareWeight', label: '皮重(吨)' },
{ prop: 'tareWeightTime', label: '皮重时间' },
{ prop: 'deduction', label: '扣杂(吨)' },
{ prop: 'remarks', label: '备注' },
{ prop: 'createByName', label: '创建人' }
]
const {
visibleColumns,
checkAll,
isIndeterminate,
isColumnVisible,
resetToDefault
} = usePersistedListColumns('db-inventory-list-columns', listColumnDefs)
const exportLoading = ref(false)
const saving = ref(false)
const tableData = ref<InventoryRecord[]>([])
const allRecords = ref<InventoryRecord[]>([])
const total = ref(0)
const currentPage = ref(1)
const pageSize = ref(20)
const activeDateShortcut = ref<'all' | 'today' | 'tomorrow' | 'yesterday' | 'week' | ''>('today')
const searchKeyword = ref('')
const selectedRows = ref<InventoryRecord[]>([])
const sortProp = ref<string>('date')
const sortOrder = ref<'ascending' | 'descending' | null>('descending')
const pad2 = (n: number) => String(n).padStart(2, '0')
const formatYmd = (d: Date) =>
`${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`
const dateRange = ref<[string, string] | null>([formatYmd(new Date()), formatYmd(new Date())])
const modalOpen = ref(false)
const editingRecord = ref<InventoryRecord | null>(null)
const relatedModalOpen = ref(false)
const loadingRelated = ref(false)
const relatedData = ref<InventoryRelatedData>({ plan: null, weighing: null })
const viewingRecord = ref<InventoryRecord | null>(null)
async function refreshRelatedViewIfOpen(editedId?: string) {
if (!relatedModalOpen.value || !viewingRecord.value?.id) return
if (editedId && viewingRecord.value.id !== editedId) return
syncViewRowFromFlatList(viewingRecord, allRecords.value)
relatedData.value = await inventoryApi.getRelated(viewingRecord.value.id)
}
const filters = reactive({
batchNo: '',
businessType: '' as '' | '入库' | '出库',
status: '',
vehicleNo: ''
})
const businessTypeOptions: Array<'入库' | '出库'> = ['入库', '出库']
const statusOptions = ['待处理', '待入库', '待出库', '待三方填写', '已完成']
const isFromPlanJump = ref(false)
const openedFromPlan = ref(false)
const qrcodeModalOpen = ref(false)
const qrcodeInventoryId = ref('')
const formRef = ref()
const formData = reactive<
InventoryCreate & {
product: string
specification: string
}
>({
date: '',
partnerCompany: '',
businessType: '入库',
productId: '',
product: '',
specification: '',
quantity: 32,
planNo: '',
vehicleNo: '',
remarks: '',
purchasePlanId: undefined
})
const isPlanNoReadonly = computed(() => !!formData.purchasePlanId && !editingRecord.value)
const formRules = {
date: [{ required: true, message: '请选择日期', trigger: 'change' }],
partnerCompany: [{ required: true, message: '请选择往来单位', trigger: 'change' }],
productId: [{ required: true, message: '请选择上架产品', trigger: 'change' }],
quantity: [{ required: true, message: '请输入数量', trigger: 'blur' }],
vehicleNo: [{ required: true, message: '请选择车牌号', trigger: 'change' }]
}
const formatKgToTon = (kg: number | undefined | null) => {
if (kg === undefined || kg === null) return '--'
return (kg / 1000).toFixed(2)
}
const getStatusTagType = (status?: string) => {
if (status === '已完成') return 'success'
if (status === '待入库' || status === '待出库') return 'warning'
if (status === '待三方填写') return 'info'
return 'info'
}
const getSortOrder = (prop: string): 'ascending' | 'descending' | null => {
return sortProp.value === prop ? sortOrder.value : null
}
const getSummary = (param: { columns: TableColumnCtx<InventoryRecord>[]; data: InventoryRecord[] }) => {
const { columns } = param
const sums: string[] = []
columns.forEach((column, index) => {
if (column.type === 'selection') {
sums[index] = ''
return
}
if (column.type === 'index') {
sums[index] = '合计'
return
}
if (column.property === 'quantity') {
const totalQuantity = tableData.value.reduce(
(sum, row) => sum + (Number(row.quantity) || 0),
0
)
sums[index] = totalQuantity.toString()
return
}
if (
column.property === 'netWeight' ||
column.property === 'grossWeight' ||
column.property === 'tareWeight' ||
column.property === 'deduction'
) {
const prop = column.property as keyof InventoryRecord
const totalWeight = tableData.value.reduce(
(sum, row) => sum + (Number(row[prop]) || 0) / 1000,
0
)
sums[index] = totalWeight.toFixed(2)
return
}
sums[index] = ''
})
return sums
}
const compare = (
a: InventoryRecord,
b: InventoryRecord,
prop: string,
order: 'ascending' | 'descending'
): number => {
const av = (a as Record<string, unknown>)[prop]
const bv = (b as Record<string, unknown>)[prop]
const aEmpty = av === undefined || av === null
const bEmpty = bv === undefined || bv === null
const isNum = typeof av === 'number' || typeof bv === 'number'
const aVal = aEmpty ? (isNum ? Infinity : '') : av
const bVal = bEmpty ? (isNum ? Infinity : '') : bv
let cmp = 0
if (typeof aVal === 'number' && typeof bVal === 'number') {
cmp = aVal - bVal
} else {
cmp = String(aVal).localeCompare(String(bVal), 'zh-CN')
}
return order === 'ascending' ? cmp : -cmp
}
const applySortAndPage = () => {
let list = [...allRecords.value]
if (sortProp.value && sortOrder.value) {
list.sort((a, b) => compare(a, b, sortProp.value, sortOrder.value!))
}
total.value = list.length
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
tableData.value = list.slice(start, end)
}
const buildListQueryParams = (): InventoryListParams => {
const params: InventoryListParams = {}
const keyword = searchKeyword.value.trim()
if (keyword) params.q = keyword
if (dateRange.value?.[0] && dateRange.value?.[1]) {
params.dateFrom = dateRange.value[0]
params.dateTo = dateRange.value[1]
}
if (filters.batchNo.trim()) params.batchNo = filters.batchNo.trim()
if (filters.vehicleNo.trim()) params.vehicleNo = filters.vehicleNo.trim()
if (filters.businessType) params.businessType = filters.businessType
if (filters.status) params.status = filters.status
return params
}
const handleSortChange = (payload: { prop: string; order: string | null }) => {
sortProp.value = payload.prop || ''
sortOrder.value =
payload.order === 'ascending' || payload.order === 'descending' ? payload.order : null
currentPage.value = 1
applySortAndPage()
}
const loadData = async () => {
loading.value = true
try {
allRecords.value = await inventoryApi.list(buildListQueryParams())
applySortAndPage()
} 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: InventoryRecord[]) => {
selectedRows.value = selection
}
const handleResetFilters = () => {
filters.batchNo = ''
filters.businessType = ''
filters.status = ''
filters.vehicleNo = ''
searchKeyword.value = ''
dateRange.value = [formatYmd(new Date()), formatYmd(new Date())]
activeDateShortcut.value = 'today'
currentPage.value = 1
loadData()
}
const handleDateShortcut = (type: 'all' | 'today' | 'tomorrow' | 'yesterday' | 'week') => {
activeDateShortcut.value = type
const today = new Date()
if (type === 'all') {
dateRange.value = null
} else if (type === 'today') {
const d = formatYmd(today)
dateRange.value = [d, d]
} else if (type === 'tomorrow') {
const t = new Date(today)
t.setDate(t.getDate() + 1)
const d = formatYmd(t)
dateRange.value = [d, d]
} else if (type === 'yesterday') {
const y = new Date(today)
y.setDate(y.getDate() - 1)
const d = formatYmd(y)
dateRange.value = [d, d]
} else 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)
dateRange.value = [formatYmd(monday), formatYmd(sunday)]
}
handleSearch()
}
const handleDateRangeChange = () => {
activeDateShortcut.value = ''
currentPage.value = 1
}
const handlePageChange = (page: number) => {
currentPage.value = page
applySortAndPage()
}
const handleSizeChange = (size: number) => {
pageSize.value = size
currentPage.value = 1
applySortAndPage()
}
const clearPlanRouteQuery = () => {
if (!route.query.from && !route.query.planNo) return
router.replace({ path: route.path })
}
const openAddFromPlanQuery = () => {
if (route.query.from !== 'plan' || !route.query.planNo) return
openedFromPlan.value = true
isFromPlanJump.value = true
handleAdd()
clearPlanRouteQuery()
}
const handleAdd = () => {
editingRecord.value = null
const useRouteParams = isFromPlanJump.value
Object.assign(formData, {
date: useRouteParams ? '' : formatYmd(new Date()),
partnerCompany: useRouteParams ? (route.query.customerName as string) || '' : '',
businessType: useRouteParams ? (route.query.businessType as '入库' | '出库') || '入库' : '入库',
productId: useRouteParams ? (route.query.productId as string) || '' : '',
product: useRouteParams ? (route.query.product as string) || '' : '',
specification: useRouteParams ? (route.query.specification as string) || '' : '',
quantity: 32,
planNo: useRouteParams ? (route.query.planNo as string) || '' : '',
vehicleNo: '',
remarks: '',
purchasePlanId: useRouteParams ? (route.query.purchasePlanId as string) || undefined : undefined
})
isFromPlanJump.value = false
modalOpen.value = true
}
const handleView = async (record: InventoryRecord) => {
viewingRecord.value = record
loadingRelated.value = true
try {
relatedData.value = await inventoryApi.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 handleToolbarDelete() {
if (selectedRows.value.length !== 1) {
$modal.msgWarning('请先勾选一条记录')
return
}
handleDelete(selectedRows.value[0])
}
async function handleViewDelete() {
if (!viewingRecord.value) return
await handleDelete(viewingRecord.value)
relatedModalOpen.value = false
}
const handleEdit = (record: InventoryRecord) => {
editingRecord.value = record
Object.assign(formData, {
date: record.date,
partnerCompany: record.partnerCompany,
businessType: record.businessType,
productId: record.productId || '',
product: record.product,
specification: record.specification || '',
quantity: record.quantity,
planNo: record.planNo || '',
vehicleNo: record.vehicleNo,
remarks: record.remarks || '',
purchasePlanId: record.purchasePlanId
})
modalOpen.value = true
}
const canDeleteRecord = (row: InventoryRecord) => {
if (row.status === '已完成') return false
const dateStr = row.date || ''
if (!dateStr) return false
return dateStr >= formatYmd(new Date())
}
const getDeleteRecordDisabledTitle = (row: InventoryRecord) => {
if (row.status === '已完成') return '已完成的记录不能删除'
if (!canDeleteByCreator(row)) return CREATOR_DELETE_TIP
return '仅支持删除当天及之后日期的记录,历史数据不可删除'
}
const handleDelete = async (row: InventoryRecord) => {
if (row.status === '已完成') {
$modal.msgWarning('已完成的记录不能删除')
return
}
if (!canDeleteRecord(row)) {
$modal.msgWarning('仅支持删除当天及之后日期的记录,历史数据不可删除')
return
}
if (!canDeleteByCreator(row)) {
$modal.msgWarning(CREATOR_DELETE_TIP)
return
}
try {
await $modal.confirm('确定要删除这条记录吗?')
await inventoryApi.delete(row.id)
$modal.msgSuccess('删除成功')
loadData()
} catch {
// 用户取消或请求失败
}
}
const handleGenerateWeighing = (record: InventoryRecord) => {
const supplier =
record.businessType === '入库' ? record.partnerCompany : '安庆市山峡化工有限公司'
const consignee =
record.businessType === '入库' ? '安庆市山峡化工有限公司' : record.partnerCompany
router.push({
path: '/db/weighing',
query: {
inboundOutboundId: record.id,
vehicleNo: record.vehicleNo,
cargoName: record.product,
productId: record.productId || '',
specification: record.specification || '',
businessType: record.businessType,
supplier,
consignee,
planNo: record.planNo || '',
purchasePlanId: record.purchasePlanId || '',
batchNo: record.batchNo || '',
quantity: record.quantity.toString()
}
})
}
const handleCopyFillLink = async (row: InventoryRecord) => {
qrcodeInventoryId.value = row.id
qrcodeModalOpen.value = true
}
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid: boolean) => {
if (!valid) return
saving.value = true
try {
const cleanData = (data: Record<string, unknown>) => {
const cleaned: Record<string, unknown> = {}
for (const key in data) {
const value = data[key]
if (value !== '' && value !== undefined && value !== null) {
cleaned[key] = value
}
}
return cleaned
}
if (editingRecord.value) {
const editedId = editingRecord.value.id
const updateData = cleanData({
date: formData.date,
partnerCompany: formData.partnerCompany,
businessType: formData.businessType,
productId: formData.productId,
quantity: formData.quantity,
planNo: formData.planNo || undefined,
vehicleNo: formData.vehicleNo,
purchasePlanId: formData.purchasePlanId
})
updateData.remarks = formData.remarks === '' ? null : formData.remarks || undefined
await inventoryApi.update(editingRecord.value.id, updateData)
$modal.msgSuccess('更新成功')
modalOpen.value = false
if (openedFromPlan.value) {
openedFromPlan.value = false
router.replace('/db/plans')
} else {
await loadData()
await refreshRelatedViewIfOpen(editedId)
}
} else {
const createData: InventoryCreate = {
date: formData.date,
partnerCompany: formData.partnerCompany,
businessType: formData.businessType,
productId: formData.productId,
quantity: formData.quantity,
vehicleNo: formData.vehicleNo,
planNo: formData.planNo || undefined,
remarks: formData.remarks || undefined,
purchasePlanId: formData.purchasePlanId
}
await inventoryApi.create(createData)
$modal.msgSuccess('保存成功')
modalOpen.value = false
if (openedFromPlan.value) {
openedFromPlan.value = false
router.replace('/db/plans')
} else {
loadData()
}
}
} catch (error) {
console.error('保存出入库记录失败', error)
} finally {
saving.value = false
}
})
}
const handleModalClose = () => {
modalOpen.value = false
editingRecord.value = null
isFromPlanJump.value = false
formRef.value?.resetFields()
const shouldBackToPlans = openedFromPlan.value
openedFromPlan.value = false
if (shouldBackToPlans) {
router.replace('/db/plans')
}
}
const handleExport = async () => {
exportLoading.value = true
try {
let records = await inventoryApi.list(buildListQueryParams())
if (records.length === 0) {
$modal.msgWarning('没有数据可导出')
return
}
let list = [...records]
if (sortProp.value && sortOrder.value) {
list.sort((a, b) => compare(a, b, sortProp.value, sortOrder.value!))
}
const headers = ['日期', '往来单位', '业务类型', '产品', '数量(吨)', '车牌号', '计划号', '备注']
const csvContent = [
headers.join(','),
...list.map((r) =>
[
r.date,
r.partnerCompany,
r.businessType,
r.product,
r.quantity,
r.vehicleNo,
r.planNo || '',
r.remarks || ''
].join(',')
)
].join('\n')
const blob = new Blob(['\uFEFF' + csvContent], { type: 'text/csv;charset=utf-8' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
const now = new Date()
const stamp = `${now.getFullYear()}${pad2(now.getMonth() + 1)}${pad2(now.getDate())}_${pad2(now.getHours())}${pad2(now.getMinutes())}${pad2(now.getSeconds())}`
a.download = `出入库列表_${stamp}.csv`
a.click()
URL.revokeObjectURL(url)
$modal.msgSuccess('导出成功')
} catch (error) {
console.error('导出失败', error)
} finally {
exportLoading.value = false
}
}
useDbInstantSearch(
() => ({
q: searchKeyword.value,
dateRange: dateRange.value,
batchNo: filters.batchNo,
businessType: filters.businessType,
status: filters.status,
vehicleNo: filters.vehicleNo
}),
handleSearch
)
onMounted(() => {
loadData()
})
onActivated(() => {
openAddFromPlanQuery()
})
</script>