Files
zkxw/web-admin/src/views/cell-storage/material/MaterialUsageTab.vue
T
2026-06-16 17:10:01 +08:00

636 lines
21 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.
<!-- 市场物料库 · 变动记录 Tab -->
<!-- eslint-disable vue/no-v-model-argument -->
<template>
<div class="material-usage-tab">
<div class="filter-block">
<el-form :model="searchForm" class="filter-form" @submit.prevent>
<div class="filter-grid">
<el-form-item label="关键词">
<el-input
v-model="searchForm.keyword"
placeholder="物料编码/名称/入库编号/批号/货号"
clearable
/>
</el-form-item>
<el-form-item label="物料筛选">
<el-select
v-model="searchForm.materialId"
placeholder="输入物料名称检索"
clearable
filterable
remote
reserve-keyword
:loading="materialOptionsLoading"
:remote-method="remoteSearchMaterials"
@visible-change="onMaterialSelectVisible"
>
<el-option
v-for="m in materialOptions"
:key="m.id"
:label="m.label"
:value="m.id"
/>
</el-select>
</el-form-item>
<el-form-item label="变动类型">
<el-select v-model="searchForm.changeType" placeholder="全部" clearable>
<el-option v-if="!mineOnly" label="入库" value="in" />
<el-option label="出库" value="out" />
<el-option label="废弃" value="discard" />
</el-select>
</el-form-item>
<el-form-item label="时间范围">
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
clearable
style="width: 100%"
/>
</el-form-item>
</div>
<div class="filter-actions-row">
<span class="filter-hint">{{
mineOnly
? '查看本人物料领用记录,可按物料、批号、类型与时间范围筛选。'
: '按物料、入库编号、批号、变动类型和时间范围查询变动记录。'
}}</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset">
<el-icon><RefreshLeft /></el-icon>重置
</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch">
<el-icon><Search /></el-icon>搜索
</el-button>
</div>
</div>
</el-form>
</div>
<div class="list-body list-wrap">
<el-table
class="list-table"
:data="tableData"
v-loading="loading"
stripe
row-key="id"
@row-dblclick="handleView"
>
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
header-align="center"
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
/>
<el-table-column
prop="materialCode"
label="物料编码"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="materialName"
label="物料名称"
min-width="140"
align="center"
header-align="center"
/>
<el-table-column
prop="stockInNo"
label="入库编号"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.stockInNo || '—' }}</template>
</el-table-column>
<el-table-column label="入库日期" min-width="110" align="center" header-align="center">
<template #default="{ row }">{{ row.stockInDate || '—' }}</template>
</el-table-column>
<el-table-column label="货号" min-width="100" align="center" header-align="center">
<template #default="{ row }">{{ row.catalogNo || '—' }}</template>
</el-table-column>
<el-table-column label="批号" min-width="100" align="center" header-align="center">
<template #default="{ row }">{{ row.batchNo || '—' }}</template>
</el-table-column>
<el-table-column label="厂家" min-width="120" align="center" header-align="center" show-overflow-tooltip>
<template #default="{ row }">{{ row.manufacturer || '—' }}</template>
</el-table-column>
<el-table-column label="单价(元)" min-width="110" align="center" header-align="center">
<template #default="{ row }">
{{ row.unitPrice != null ? formatMoneyDisplay(row.unitPrice) : '—' }}
</template>
</el-table-column>
<el-table-column label="金额(元)" min-width="110" align="center" header-align="center">
<template #default="{ row }">
{{ row.amount != null ? formatMoneyDisplay(row.amount) : '—' }}
</template>
</el-table-column>
<el-table-column
prop="changeType"
label="变动类型"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag :type="getChangeTypeTagType(row)" size="small">
{{ getChangeTypeLabel(row) }}
</el-tag>
</template>
</el-table-column>
<el-table-column
prop="changeQty"
label="领用数量"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">
{{ displayUsageQty(row) }}
</template>
</el-table-column>
<el-table-column label="变动前库存" min-width="120" align="center" header-align="center">
<template #default="{ row }">
{{ displayStockBefore(row) }}
</template>
</el-table-column>
<el-table-column
prop="stockAfter"
label="变动后库存"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
v-if="mineOnly"
label="使用状态"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">
<template v-if="isManualOutRow(row)">
<el-tag v-if="row.requisitionStatus === 'closed'" type="success" size="small">
已使用完
</el-tag>
<el-tag v-else type="info" size="small">
使用中
</el-tag>
</template>
<span v-else>—</span>
</template>
</el-table-column>
<el-table-column
prop="sourceType"
label="来源类型"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag :type="getSourceTagType(row.sourceType)" size="small">
{{ getSourceLabel(row.sourceType) }}
</el-tag>
</template>
</el-table-column>
<el-table-column
v-if="!mineOnly"
label="领用人"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">
{{ displayReceiver(row) }}
</template>
</el-table-column>
<el-table-column
label="用途"
min-width="120"
align="center"
header-align="center"
show-overflow-tooltip
>
<template #default="{ row }">
{{ displayPurpose(row) }}
</template>
</el-table-column>
<el-table-column label="领用时间" min-width="170" align="center" header-align="center">
<template #default="{ row }">
{{ displayUseTime(row) }}
</template>
</el-table-column>
<el-table-column
prop="remark"
label="备注"
min-width="140"
align="center"
header-align="center"
show-overflow-tooltip
/>
<el-table-column
prop="createByName"
label="操作人"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">
{{ row.createByName || row.createBy || '-' }}
</template>
</el-table-column>
<el-table-column
prop="createTime"
label="变动时间"
min-width="170"
align="center"
header-align="center"
/>
<el-table-column
label="操作"
min-width="260"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<div class="table-op-btns">
<el-button class="btn-op-item" @click="handleView(row)">
<el-icon><View /></el-icon>查看
</el-button>
<el-button
v-if="canMarkFullyUsed(row)"
class="btn-op-item"
:loading="markFullyUsedLoadingId === row.id"
@click="handleMarkFullyUsed(row)"
>
<el-icon><CircleCheck /></el-icon>标记已使用完
</el-button>
</div>
</template>
</el-table-column>
</el-table>
<div class="pagination list-pager-bar">
<el-pagination
:current-page="pagination.page"
:page-size="pagination.pageSize"
:total="pagination.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next"
@size-change="(val) => ((pagination.page = 1), (pagination.pageSize = val), loadData())"
@current-change="(val) => ((pagination.page = val), loadData())"
/>
</div>
</div>
<!-- 查看弹窗 -->
<SystemViewDialog
v-model:visible="viewVisible"
:title="viewRow?.materialName || '变动记录详情'"
:icon="Document"
:meta="viewRow?.createTime || ''"
:sections="viewSections"
:loading="viewLoading"
width="960px"
/>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, RefreshLeft, View, Document, CircleCheck } from '@element-plus/icons-vue'
import {
fetchMaterialUsageList,
fetchMyMaterialUsageList,
fetchMarkMaterialUsageFullyUsed
} from '@/api/material-usage'
import type { MaterialUsage } from '@/api/material-usage'
import { fetchMaterialList } from '@/api/material'
import type { MaterialOption } from '@/api/material'
import { formatMoneyDisplay } from '@/utils/money-format'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
defineOptions({ name: 'MaterialUsageTab' })
const props = withDefaults(
defineProps<{
mineOnly?: boolean
}>(),
{
mineOnly: false
}
)
const route = useRoute()
const searchForm = reactive<{
keyword: string
materialId: string
changeType: string
}>({
keyword: '',
materialId: '',
changeType: ''
})
const dateRange = ref<[string, string] | null>(null)
const tableData = ref<MaterialUsage[]>([])
const loading = ref(false)
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
const materialOptions = ref<MaterialOption[]>([])
const materialOptionsLoading = ref(false)
function isManualOutRow(row: MaterialUsage) {
if (row.sourceType === 'manual_out') return true
const remark = row.remark || ''
return row.changeType === 'use' && (row.changeQty ?? 0) < 0 && remark.includes('领用人:')
}
function isDiscardRow(row: MaterialUsage) {
return row.changeType === 'discard' || row.sourceType === 'manual_discard'
}
function parseFromRemark(remark: string, key: 'receiver' | 'purpose') {
const prefix = key === 'receiver' ? '领用人:' : '用途:'
for (const part of remark.split(';')) {
const seg = part.trim()
if (seg.startsWith(prefix)) return seg.slice(prefix.length).trim()
}
return ''
}
function displayReceiver(row: MaterialUsage) {
if (isDiscardRow(row)) return '-'
if (!isManualOutRow(row)) return '-'
return (row.receiver || '').trim() || parseFromRemark(row.remark || '', 'receiver') || '-'
}
function displayPurpose(row: MaterialUsage) {
if (isDiscardRow(row)) return (row.purpose || '').trim() || '-'
if (!isManualOutRow(row)) return '-'
return (row.purpose || '').trim() || parseFromRemark(row.remark || '', 'purpose') || '-'
}
function displayUseTime(row: MaterialUsage) {
if (isDiscardRow(row)) return (row.createTime || '').trim() || '-'
if (!isManualOutRow(row)) return '-'
return (row.useTime || row.createTime || '').trim() || '-'
}
function displayUsageQty(row: MaterialUsage) {
const qty = Math.abs(Number(row.changeQty) || 0)
const unit = (row.usageUnit || '').trim()
return unit ? `${qty}${unit}` : String(qty)
}
function displayStockBefore(row: MaterialUsage) {
if (row.stockBefore != null) return row.stockBefore
const after = Number(row.stockAfter ?? 0)
const qty = Number(row.changeQty ?? 0)
return after - qty
}
function getChangeTypeLabel(row: MaterialUsage) {
if (row.changeType === 'in') return '入库'
if (isDiscardRow(row)) return '废弃'
if (isManualOutRow(row)) return '出库'
return '出库'
}
function getChangeTypeTagType(row: MaterialUsage) {
if (row.changeType === 'in') return 'success'
if (isDiscardRow(row)) return 'danger'
if (isManualOutRow(row)) return 'warning'
return 'warning'
}
function getSourceTagType(sourceType: string) {
if (sourceType === 'manual_in') return 'success'
if (sourceType === 'manual_out') return 'warning'
if (sourceType === 'manual_discard') return 'danger'
if (sourceType === 'outbound_handover') return 'primary'
return 'info'
}
function getSourceLabel(sourceType: string) {
if (sourceType === 'manual_in') return '手动入库'
if (sourceType === 'manual_out') return '手动出库'
if (sourceType === 'manual_discard') return '手动废弃'
if (sourceType === 'outbound_handover') return '出库交接'
return sourceType || '-'
}
const markFullyUsedLoadingId = ref('')
function canMarkFullyUsed(row: MaterialUsage) {
return isManualOutRow(row) && (row.requisitionStatus || 'open') !== 'closed'
}
async function handleMarkFullyUsed(row: MaterialUsage) {
try {
await ElMessageBox.confirm('确认该领用记录已全部使用完?标记后将不再出现在盘点待登记列表。', '标记已使用完', {
type: 'warning',
confirmButtonText: '确认',
cancelButtonText: '取消'
})
} catch {
return
}
markFullyUsedLoadingId.value = row.id
try {
await fetchMarkMaterialUsageFullyUsed(row.id)
row.requisitionStatus = 'closed'
ElMessage.success('已标记为使用完')
} finally {
markFullyUsedLoadingId.value = ''
}
}
function mapMaterialListToOptions(list: Array<Record<string, any>>): MaterialOption[] {
return (list || []).map((m) => ({
id: String(m.id || ''),
label: `${m.materialName || ''}${m.materialCode ? ` (${m.materialCode})` : ''}`.trim(),
materialCode: String(m.materialCode || ''),
materialName: String(m.materialName || ''),
category: String(m.category || ''),
specModel: String(m.specModel || ''),
unit: String(m.unit || ''),
stockQty: Number(m.stockQty ?? 0)
}))
}
async function remoteSearchMaterials(query: string) {
materialOptionsLoading.value = true
try {
const res = await fetchMaterialList({
page: 1,
pageSize: 100,
keyword: query?.trim() || undefined
})
materialOptions.value = mapMaterialListToOptions(res.list || [])
} catch {
materialOptions.value = []
} finally {
materialOptionsLoading.value = false
}
}
function onMaterialSelectVisible(visible: boolean) {
if (visible) void remoteSearchMaterials('')
}
async function loadData() {
loading.value = true
try {
const params: Record<string, any> = {
page: pagination.page,
pageSize: pagination.pageSize
}
if (searchForm.keyword) params.keyword = searchForm.keyword
if (searchForm.materialId) params.materialId = searchForm.materialId
if (searchForm.changeType) params.changeType = searchForm.changeType
if (dateRange.value && dateRange.value[0]) {
params.startTime = dateRange.value[0]
params.endTime = dateRange.value[1]
}
const res = props.mineOnly
? await fetchMyMaterialUsageList(params)
: await fetchMaterialUsageList(params)
tableData.value = res.list || []
pagination.total = res.total ?? 0
} finally {
loading.value = false
}
}
function handleSearch() {
pagination.page = 1
loadData()
}
function handleReset() {
searchForm.keyword = ''
searchForm.materialId = ''
searchForm.changeType = ''
dateRange.value = null
pagination.page = 1
loadData()
}
const viewVisible = ref(false)
const viewLoading = ref(false)
const viewRow = ref<MaterialUsage | null>(null)
const viewSections = computed(() => {
if (!viewRow.value) return []
const row = viewRow.value
return [
{
title: '变动信息',
fields: [
{ label: '物料编码', value: row.materialCode },
{ label: '物料名称', value: row.materialName },
{ label: '入库编号', value: row.stockInNo || '—' },
{ label: '入库日期', value: row.stockInDate || '—' },
{ label: '货号', value: row.catalogNo || '—' },
{ label: '批号', value: row.batchNo || '—' },
{ label: '厂家', value: row.manufacturer || '—' },
{
label: '单价(元)',
value: row.unitPrice != null ? formatMoneyDisplay(row.unitPrice) : '—'
},
{
label: '金额(元)',
value: row.amount != null ? formatMoneyDisplay(row.amount) : '—'
},
{
label: '变动类型',
value: getChangeTypeLabel(row),
tagType: getChangeTypeTagType(row) as any
},
{
label: isManualOutRow(row) ? '领用数量' : '变动数量',
value: isManualOutRow(row)
? displayUsageQty(row)
: row.changeQty < 0
? String(row.changeQty)
: `+${row.changeQty}`
},
{ label: '变动前库存', value: String(displayStockBefore(row)) },
{ label: '变动后库存', value: String(row.stockAfter ?? 0) },
{
label: '来源类型',
value: getSourceLabel(row.sourceType),
tagType: getSourceTagType(row.sourceType) as any
},
{
label: '领用人',
value: displayReceiver(row) === '-' ? '—' : displayReceiver(row)
},
{
label: '用途',
value: displayPurpose(row) === '-' ? '—' : displayPurpose(row)
},
{
label: '领用时间',
value: displayUseTime(row) === '-' ? '—' : displayUseTime(row)
},
{ label: '备注', value: row.remark || '—', span: true },
{ label: '操作人', value: row.createByName || row.createBy || '—' },
{ label: '变动时间', value: row.createTime || '—' }
]
}
]
})
function handleView(row: MaterialUsage) {
viewRow.value = row
viewVisible.value = true
}
// 输入检索条件时自动搜索(防抖 300ms)
watchDebounced(
searchForm,
() => {
pagination.page = 1
loadData()
},
{ debounce: 300, deep: true }
)
// 日期范围变更时自动搜索
watch(dateRange, () => {
pagination.page = 1
loadData()
})
onMounted(async () => {
if (route.query.materialId) {
searchForm.materialId = route.query.materialId as string
}
if (route.query.stockInNo) {
searchForm.keyword = route.query.stockInNo as string
}
await remoteSearchMaterials('')
loadData()
})
</script>
<style scoped lang="scss">
.table-op-btns {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 6px;
}
</style>