1170 lines
39 KiB
Vue
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>
|