Files
bj_power/bj_power_wms_client/frontend/src/pages/Stocktake.vue
T

238 lines
9.6 KiB
Vue
Raw Normal View History

2026-08-28 15:06:01 +08:00
<script setup>
import { nextTick, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import request from '../utils/request'
import { fmtTime } from '../utils/format'
import { getRealName } from '../utils/auth'
import PageHelp from '../components/PageHelp.vue'
import { helpStocktake } from '../help'
2026-08-28 15:06:01 +08:00
/* ---------- 发起盘点(动态/静态/抽盘通过后端类型扩展,当前按全量快照) ---------- */
const operator = getRealName()
const starting = ref(false)
const active = reactive({ no: '', totalTargets: 0 })
async function startStocktake() {
starting.value = true
try {
const data = await request.post('/stocktake/start', { operator })
active.no = data?.stocktakeNo || ''
active.totalTargets = data?.totalTargets || 0
ElMessage.success(`盘点已发起:盘点单号 ${active.no},目标 ${active.totalTargets} 项`)
await loadHistory()
} finally {
starting.value = false
}
}
/* ---------- 扫码快速盘点 ---------- */
const recordForm = reactive({ targetId: '', scanQty: 1 })
const recording = ref(false)
const idRef = ref()
const records = ref([])
async function submitRecord() {
if (!active.no) {
ElMessage.warning('请先发起或填写盘点单号')
return
}
const targetId = recordForm.targetId.trim()
if (!targetId) {
ElMessage.warning('请扫入目标编号(批次号或 SN)')
return
}
const qty = Number(recordForm.scanQty)
if (!Number.isInteger(qty) || qty < 0) {
ElMessage.warning('实盘数量需为非负整数')
return
}
recording.value = true
try {
const data = await request.post('/stocktake/record', {
stocktakeNo: active.no,
targetId,
scanQty: qty
})
const hit = records.value.findIndex(r => r.targetId === data?.targetId)
const row = {
targetId: data?.targetId ?? targetId,
bookQty: data?.bookQty ?? '-',
scannedQty: data?.scannedQty ?? qty,
diffQty: data?.diffQty ?? qty - (data?.bookQty ?? 0)
}
if (hit >= 0) records.value.splice(hit, 1, row)
else records.value.unshift(row)
ElMessage.success(
`已记录 ${row.targetId}:账面 ${row.bookQty},实盘 ${row.scannedQty},差异 ${row.diffQty}`
)
recordForm.targetId = ''
nextTick(() => idRef.value?.focus())
} finally {
recording.value = false
}
}
/* ---------- 完成盘点 ---------- */
const adjust = ref(true)
const finishing = ref(false)
const finishResult = ref(null)
async function finishStocktake() {
if (!active.no) {
ElMessage.warning('请先发起盘点')
return
}
finishing.value = true
try {
const data = await request.post('/stocktake/finish', { stocktakeNo: active.no, adjust: adjust.value })
finishResult.value = data || {}
ElMessage.success(
`盘点完成:已盘 ${data?.counted ?? 0} 项,未盘 ${data?.uncounted ?? 0} 项` +
(adjust.value ? ',差异已写回库存' : '')
)
records.value = []
await loadHistory()
} finally {
finishing.value = false
}
}
/* ---------- 历史盘点单 ---------- */
const historyFilter = ref('')
const history = ref([])
const historyLoading = ref(false)
async function loadHistory() {
historyLoading.value = true
try {
const params = {}
if (historyFilter.value.trim()) params.stocktakeNo = historyFilter.value.trim()
const data = await request.get('/stocktake/query', { params })
history.value = data?.list || []
} finally {
historyLoading.value = false
}
}
</script>
<template>
<div>
<el-row :gutter="12">
<el-col :span="10">
<el-card shadow="never" class="mb12" header="发起盘点">
<el-descriptions :column="1" border size="small">
<el-descriptions-item label="操作人">{{ operator || '-' }}</el-descriptions-item>
<el-descriptions-item label="盘点单号">{{ active.no || '未发起' }}</el-descriptions-item>
<el-descriptions-item label="目标数量">{{ active.no ? `${active.totalTargets} 项` : '-' }}</el-descriptions-item>
</el-descriptions>
<el-button type="primary" class="mt12" :loading="starting"
:disabled="!!active.no && !finishResult" @click="startStocktake">
发起盘点冻结库存快照
</el-button>
</el-card>
<el-card shadow="never" header="完成盘点">
<el-form label-width="120px">
<el-form-item label="差异写回库存">
<el-switch v-model="adjust" />
<span class="tip">开启后按实盘差异修正库存</span>
</el-form-item>
<el-form-item>
<el-button type="warning" :loading="finishing" @click="finishStocktake">完成并生成差异</el-button>
</el-form-item>
</el-form>
</el-card>
</el-col>
<el-col :span="14">
<el-card shadow="never" class="mb12" header="扫码快速盘点(回车即提交并清空)">
<template v-if="!active.no">
<el-input v-model="active.no" placeholder="也可粘贴已有进行中的盘点单号" clearable>
<template #prepend>盘点单号</template>
</el-input>
</template>
<el-form inline @submit.prevent="submitRecord">
<el-form-item label="目标编号">
<el-input ref="idRef" v-model="recordForm.targetId" clearable autofocus placeholder="批次号 / SN"
style="width:220px" @keyup.enter="submitRecord" />
</el-form-item>
<el-form-item label="实盘数量">
<el-input-number v-model="recordForm.scanQty" :min="0" :step="1" step-strictly style="width:120px" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="recording" @click="submitRecord">提交</el-button>
</el-form-item>
</el-form>
<el-table :data="records" border size="small" max-height="300" empty-text="尚无扫码记录">
<el-table-column prop="targetId" label="目标编号" min-width="170" />
<el-table-column prop="bookQty" label="账面数" width="90" align="center" />
<el-table-column prop="scannedQty" label="实盘数" width="90" align="center" />
<el-table-column label="差异" width="90" align="center">
<template #default="{ row }">
<span :class="{ bad: row.diffQty !== 0 }">{{ row.diffQty }}</span>
</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
</el-row>
<el-card v-if="finishResult" shadow="never" class="mb12" header="差异明细(diffs">
<el-alert type="info" :closable="false"
:title="`盘点单 ${finishResult.stocktakeNo}:已盘 ${finishResult.counted} 项 / 未盘 ${finishResult.uncounted} 项${finishResult.adjusted ? ',差异已写回库存' : ''}`"
style="margin-bottom:10px" />
<el-table :data="finishResult.diffs || []" border size="small"
empty-text="无差异账实相符">
<el-table-column prop="targetId" label="目标编号" min-width="190" />
<el-table-column prop="type" label="类型" width="100" align="center">
<template #default="{ row }">{{ row.type === 'BATCH' ? '批次' : 'SN' }}</template>
</el-table-column>
<el-table-column prop="bookQty" label="账面数" width="100" align="center" />
<el-table-column prop="scannedQty" label="实盘数" width="100" align="center" />
<el-table-column prop="diffQty" label="差异" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.diffQty > 0 ? 'success' : 'danger'" size="small">{{ row.diffQty }}</el-tag>
</template>
</el-table-column>
</el-table>
</el-card>
<el-card shadow="never" header="历史盘点单查询">
<el-form inline @submit.prevent="loadHistory">
<el-form-item label="盘点单号">
<el-input v-model="historyFilter" placeholder="留空查最近全部" clearable style="width:240px"
@keyup.enter="loadHistory" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="historyLoading" @click="loadHistory">查询</el-button>
</el-form-item>
</el-form>
<el-table :data="history" border size="small" empty-text="暂无盘点记录">
<el-table-column prop="stocktakeNo" label="盘点单号" min-width="200" />
<el-table-column prop="operator" label="操作人" width="110" align="center">
<template #default="{ row }">{{ row.operator || '-' }}</template>
</el-table-column>
<el-table-column prop="totalTargets" label="目标数" width="90" align="center" />
<el-table-column label="状态" width="110" align="center">
<template #default="{ row }">
<el-tag :type="row.status === '已完成' ? 'success' : 'primary'" size="small">{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column label="发起时间" width="160" align="center">
<template #default="{ row }">{{ fmtTime(row.createdAt) }}</template>
</el-table-column>
<el-table-column label="完成时间" width="160" align="center">
<template #default="{ row }">{{ fmtTime(row.finishedAt) }}</template>
</el-table-column>
</el-table>
</el-card>
</div>
<PageHelp :data="helpStocktake" />
2026-08-28 15:06:01 +08:00
</template>
<style scoped>
.mb12 { margin-bottom: 12px; }
.mt12 { margin-top: 12px; }
.tip { margin-left: 10px; color: #909399; font-size: 12px; }
.bad { color: #f56c6c; font-weight: 700; }
</style>