Browse Source

2025-10-27

仓库盘点管理PDA功能
master
fengyuan_yang 3 months ago
parent
commit
9b0ef6c1fd
  1. 57
      src/api/warehouse/countingWIP.js
  2. 10
      src/router/index.js
  3. 4
      src/views/main.vue
  4. 741
      src/views/modules/warehouse/countingDetail.vue
  5. 368
      src/views/modules/warehouse/countingList.vue

57
src/api/warehouse/countingWIP.js

@ -0,0 +1,57 @@
import http from '@/utils/httpRequest'
/**
* 查询当天的盘点单列表
*/
export function countingWIPTodayList(data) {
return http({
url: http.adornUrl('/warehouse/countingWIP/todayList'),
method: 'post',
data: http.adornData(data)
})
}
/**
* 根据盘点单号查询WIP数据
*/
export function countingWIPList(data) {
return http({
url: http.adornUrl('/warehouse/countingWIP/wipList'),
method: 'post',
data: http.adornData(data)
})
}
/**
* 扫描标签
*/
export function countingWIPScanLabel(data) {
return http({
url: http.adornUrl('/warehouse/countingWIP/scanLabel'),
method: 'post',
data: http.adornData(data)
})
}
/**
* 更新盘点数量
*/
export function countingWIPUpdateCheckedQty(data) {
return http({
url: http.adornUrl('/warehouse/countingWIP/updateCheckedQty'),
method: 'post',
data: http.adornData(data)
})
}
/**
* 确认盘点
*/
export function countingWIPConfirmCounting(data) {
return http({
url: http.adornUrl('/warehouse/countingWIP/confirmCounting'),
method: 'post',
data: http.adornData(data)
})
}

10
src/router/index.js

@ -148,6 +148,16 @@ const globalRoutes = [
component: resolve => require(["@/views/modules/cross-area-transfer/crossAreaTransfer.vue"], resolve), meta: { transition: 'instant', preload: true, keepAlive: true }
},
// 库内盘点
{
path: "/countingList", name: "countingList",
component: resolve => require(["@/views/modules/warehouse/countingList.vue"], resolve), meta: { transition: 'instant', preload: true, keepAlive: true }
},
{
path: "/countingDetail", name: "countingDetail",
component: resolve => require(["@/views/modules/warehouse/countingDetail.vue"], resolve), meta: { transition: 'instant', preload: true, keepAlive: true }
},
]
// 主入口路由(需嵌套上左右整体布局)

4
src/views/main.vue

@ -133,11 +133,11 @@
</div>
<div class="menu-text">跨区调拨</div>
</div>
<div class="menu-item disabled" @click="handleDisabledFeature('库内盘点')">
<div class="menu-item" @click="navigateWithWarehouseCheck('countingList')">
<div class="menu-icon inventory">
<van-icon name="records" size="24" />
</div>
<div class="menu-text" >库内盘点</div>
<div class="menu-text">库内盘点</div>
</div>
<div class="menu-item disabled" @click="handleDisabledFeature('货位移动')">
<div class="menu-icon hu">

741
src/views/modules/warehouse/countingDetail.vue

@ -0,0 +1,741 @@
<template>
<div class="pda-container">
<!-- 头部栏 -->
<div class="header-bar">
<div class="header-left" @click="$router.back()">
<i class="el-icon-arrow-left"></i>
<span>盘点</span>
</div>
<div class="header-right" @click="$router.push({ path: '/' })">
首页
</div>
</div>
<!-- 搜索框 -->
<div class="search-container">
<el-input
clearable
class="compact-input"
v-model="scanCode"
placeholder="请扫描标签条码"
prefix-icon="el-icon-search"
@keyup.enter.native="handleScan"
ref="scanInput"
/>
<div class="mode-switch">
<el-switch
class="custom-switch"
v-model="isRemoveMode"
active-color="#ff4949"
inactive-color="#13ce66">
</el-switch>
<span v-if="isRemoveMode" class="switch-text">{{ '移除' }}</span>
<span v-else class="switch-text2">{{ '添加' }}</span>
</div>
</div>
<!-- 盘点单信息卡片 -->
<div class="material-info-card" v-if="reportId">
<div class="card-details">
<div class="detail-item">
<div class="detail-label">盘点单号</div>
<div class="detail-value">{{ reportId }}</div>
</div>
<div class="detail-item">
<div class="detail-label">标签张数</div>
<div class="detail-value">
<span class="highlight">{{ checkedCount }}</span>
<span class="separator">/</span>
<span class="total-value">{{ totalCount }}</span>
</div>
</div>
<div class="detail-item">
<div class="detail-label">物料总数</div>
<div class="detail-value">
<span class="highlight">{{ checkedQty }}</span>
<span class="separator">/</span>
<span class="total-value">{{ totalQty }}</span>
</div>
</div>
</div>
</div>
<!-- 盘点信息确认标题 -->
<div class="section-title">
<div class="title-left">
<i class="el-icon-circle-check"></i>
<span>盘点信息确认</span>
</div>
</div>
<!-- 标签列表 -->
<div class="label-list">
<div class="list-header">
<div class="col-no">NO.</div>
<div class="col-label">标签条码</div>
<div class="col-roll-qty">标签数量</div>
<div class="col-status">盘点状态</div>
<div class="col-qty">盘点数量</div>
</div>
<div class="list-body">
<div
v-for="(item, index) in wipList"
:key="index"
class="list-item"
:class="'status-' + getStatusClass(item.checkedStatus)">
<div class="col-no">{{ wipList.length - index }}</div>
<div class="col-label">{{ item.rollNo }}</div>
<div class="col-roll-qty">{{ item.rollQty }}</div>
<div class="col-status">
<span class="status-badge" :class="'badge-' + getStatusClass(item.checkedStatus)">
{{ item.checkedStatus }}
</span>
</div>
<div class="col-qty">
<el-input-number
v-model="item.checkedQty"
:min="0"
:precision="2"
size="mini"
controls-position="right"
@blur="updateQty(item)"
/>
</div>
</div>
<!-- 空状态 -->
<div v-if="wipList.length === 0 && !loading" class="empty-labels">
<p>暂无扫描标签</p>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="loading-labels">
<i class="el-icon-loading"></i>
<p>加载中...</p>
</div>
</div>
</div>
<!-- 底部操作按钮 -->
<div class="bottom-actions">
<button class="action-btn secondary" @click="handleConfirm" :disabled="confirmLoading">
<i v-if="confirmLoading" class="el-icon-loading"></i>
<span v-else>确定</span>
</button>
<button class="action-btn secondary" style="margin-left: 10px;" @click="$router.back()">
取消
</button>
</div>
</div>
</template>
<script>
import {
countingWIPList,
countingWIPScanLabel,
countingWIPUpdateCheckedQty,
countingWIPConfirmCounting
} from '@/api/warehouse/countingWIP.js'
export default {
name: 'CountingDetail',
data() {
return {
site: '',
buNo: '',
reportId: '',
scanCode: '',
wipList: [],
loading: false,
confirmLoading: false,
isRemoveMode: false,
totalCount: 0,
totalQty: 0,
checkedCount: 0,
checkedQty: 0
}
},
mounted() {
this.site = this.$route.query.site
this.buNo = this.$route.query.buNo
this.reportId = this.$route.query.reportId
//
this.totalCount = parseInt(this.$route.query.totalCount) || 0
this.totalQty = parseFloat(this.$route.query.totalQty) || 0
this.checkedCount = parseInt(this.$route.query.checkedCount) || 0
this.checkedQty = parseFloat(this.$route.query.checkedQty) || 0
if (!this.site || !this.reportId) {
this.$message.error('参数错误')
this.$router.go(-1)
return
}
//
this.$nextTick(() => {
this.focusInput()
})
this.getWIPList()
},
methods: {
// WIP
getWIPList() {
this.loading = true
const params = {
site: this.site,
buNo: this.buNo,
reportId: this.reportId
}
countingWIPList(params).then(({ data }) => {
if (data && data.code === 0) {
this.wipList = data.wipList || []
//
if (data.totalCount !== undefined) {
this.totalCount = data.totalCount || 0
this.totalQty = data.totalQty || 0
this.checkedCount = data.checkedCount || 0
this.checkedQty = data.checkedQty || 0
}
} else {
this.$message.error(data.msg || '查询失败')
}
this.loading = false
}).catch(() => {
this.loading = false
this.$message.error('查询失败')
})
},
//
handleScan() {
if (!this.scanCode || this.scanCode.trim() === '') {
this.$message.warning('请输入标签条码')
return
}
if (this.isRemoveMode) {
this.handleRemove()
} else {
this.scanLabel(this.scanCode.trim())
}
},
//
handleRemove() {
if (!this.scanCode || this.scanCode.trim() === '') {
this.$message.warning('请输入标签条码')
return
}
const rollNo = this.scanCode.trim()
const index = this.wipList.findIndex(item => item.rollNo === rollNo)
if (index !== -1) {
this.wipList.splice(index, 1)
this.$message.success('移除成功')
this.scanCode = ''
this.focusInput()
} else {
this.$message.warning('未找到该标签')
}
},
//
scanLabel(rollNo) {
const params = {
site: this.site,
reportId: this.reportId,
rollNo: rollNo
}
countingWIPScanLabel(params).then(({ data }) => {
if (data && data.code === 0) {
this.$message.success('扫描成功')
//
this.wipList.push(data.data)
this.scanCode = ''
this.focusInput()
} else {
this.$message.error(data.msg || '扫描失败')
}
}).catch(() => {
this.$message.error('扫描失败')
})
},
//
updateQty(item) {
const params = {
site: this.site,
reportId: this.reportId,
rollNo: item.rollNo,
checkedQty: parseFloat(item.checkedQty) || 0
}
countingWIPUpdateCheckedQty(params).then(({ data }) => {
if (data && data.code === 0) {
//
} else {
this.$message.error(data.msg || '更新失败')
}
}).catch(() => {
this.$message.error('更新失败')
})
},
//
handleConfirm() {
if (this.wipList.length === 0) {
this.$message.warning('没有需要确认的盘点数据')
return
}
this.$confirm('确定要提交盘点数据吗?提交后将无法修改。', '确认盘点', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.confirmLoading = true
const params = {
site: this.site,
reportId: this.reportId
}
countingWIPConfirmCounting(params).then(({ data }) => {
this.confirmLoading = false
if (data && data.code === 0) {
this.$message.success('盘点确认成功')
setTimeout(() => {
this.$router.go(-1)
}, 1500)
} else {
this.$message.error(data.msg || '确认失败')
}
}).catch(() => {
this.confirmLoading = false
this.$message.error('确认失败')
})
}).catch(() => {
//
})
},
//
getStatusClass(status) {
if (status === '正常') return 'normal'
if (status === '盘盈') return 'surplus'
if (status === '盘亏') return 'loss'
return ''
},
//
focusInput() {
this.$nextTick(() => {
if (this.$refs.scanInput) {
this.$refs.scanInput.focus()
}
})
}
}
}
</script>
<style scoped>
.pda-container {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
background: #f5f5f5;
}
/* 头部栏 */
.header-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 16px;
background: #17B3A3;
color: white;
height: 40px;
min-height: 40px;
}
.header-left {
display: flex;
align-items: center;
cursor: pointer;
font-size: 16px;
font-weight: 500;
}
.header-left i {
margin-right: 8px;
font-size: 18px;
}
.header-right {
cursor: pointer;
font-size: 16px;
font-weight: 500;
}
/* 搜索容器 */
.search-container {
padding: 12px 16px;
background: white;
display: flex;
align-items: center;
gap: 12px;
}
.compact-input {
flex: 1;
}
.mode-switch {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.switch-text,
.switch-text2 {
font-size: 14px;
font-weight: 500;
min-width: 32px;
}
.switch-text {
color: #ff4949;
}
.switch-text2 {
color: #13ce66;
}
/* 物料信息卡片 */
.material-info-card {
margin: 12px 16px;
padding: 16px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.card-details {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 8px;
}
.detail-item {
flex: 1;
text-align: center;
min-width: 0;
}
.detail-label {
font-size: 12px;
color: #999;
margin-bottom: 6px;
line-height: 1.2;
white-space: nowrap;
}
.detail-value {
font-size: 16px;
color: #333;
line-height: 1.2;
font-weight: 500;
word-break: break-all;
}
.detail-value .highlight {
color: #17B3A3;
font-weight: bold;
}
.detail-value .separator {
color: #333;
margin: 0 2px;
}
.detail-value .total-value {
color: #333;
font-weight: 500;
}
/* 区块标题 */
.section-title {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: white;
border-bottom: 1px solid #f0f0f0;
}
.title-left {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: bold;
color: #333;
}
.title-left i {
font-size: 16px;
color: #17B3A3;
}
/* 标签列表 */
.label-list {
flex: 1;
display: flex;
flex-direction: column;
margin: 0 16px 12px;
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.list-header {
display: flex;
align-items: center;
padding: 10px 12px;
background: #f8f9fa;
border-bottom: 1px solid #e0e0e0;
font-size: 12px;
font-weight: bold;
color: #666;
}
.list-body {
flex: 1;
overflow-y: auto;
}
.list-item {
display: flex;
align-items: center;
padding: 10px 12px;
border-bottom: 1px solid #f0f0f0;
font-size: 12px;
transition: background 0.2s;
}
.list-item:last-child {
border-bottom: none;
}
.list-item.status-normal {
border-left: 3px solid #67C23A;
}
.list-item.status-surplus {
border-left: 3px solid #E6A23C;
}
.list-item.status-loss {
border-left: 3px solid #F56C6C;
}
.col-no {
width: 40px;
text-align: center;
flex-shrink: 0;
}
.col-label {
flex: 1;
min-width: 80px;
padding: 0 8px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.col-roll-qty {
width: 70px;
text-align: center;
flex-shrink: 0;
}
.col-status {
width: 60px;
text-align: center;
flex-shrink: 0;
}
.col-qty {
width: 100px;
text-align: right;
flex-shrink: 0;
}
.status-badge {
display: inline-block;
padding: 2px 6px;
border-radius: 10px;
font-size: 11px;
font-weight: 500;
color: white;
}
.badge-normal {
background: #67C23A;
}
.badge-surplus {
background: #E6A23C;
}
.badge-loss {
background: #F56C6C;
}
/* 空状态 */
.empty-labels {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
color: #999;
}
.empty-labels p {
font-size: 14px;
margin: 0;
}
/* 加载状态 */
.loading-labels {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
color: #17B3A3;
}
.loading-labels i {
font-size: 24px;
margin-bottom: 12px;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.loading-labels p {
font-size: 14px;
margin: 0;
}
/* 底部操作按钮 */
.bottom-actions {
display: flex;
justify-content: center;
padding: 12px 16px;
background: white;
border-top: 1px solid #e0e0e0;
}
.action-btn {
flex: 1;
padding: 12px 20px;
border: none;
border-radius: 6px;
font-size: 15px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.action-btn.secondary {
background: #17B3A3;
color: white;
}
.action-btn.secondary:active {
background: #149a8b;
}
.action-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.action-btn i {
font-size: 14px;
}
/* 响应式设计 */
@media (max-width: 360px) {
.header-bar {
padding: 8px 12px;
}
.search-container {
padding: 8px 12px;
}
.material-info-card {
margin: 8px 12px;
padding: 10px 12px;
}
.label-list {
margin: 0 12px 8px;
}
.list-header,
.list-item {
padding: 8px 10px;
}
.col-no {
width: 35px;
}
.col-label {
min-width: 70px;
}
.col-roll-qty {
width: 60px;
}
.col-status {
width: 50px;
}
.col-qty {
width: 85px;
}
.bottom-actions {
padding: 10px 12px;
}
.action-btn {
padding: 10px 16px;
font-size: 14px;
}
}
</style>

368
src/views/modules/warehouse/countingList.vue

@ -0,0 +1,368 @@
<template>
<div class="pda-container">
<!-- 头部栏 -->
<div class="header-bar">
<div class="header-left" @click="$router.back()">
<i class="el-icon-arrow-left"></i>
<span>仓库盘点</span>
</div>
<div class="header-right" @click="$router.push({ path: '/' })">
首页
</div>
</div>
<!-- 搜索框 -->
<div class="search-container">
<el-input
clearable
v-model="searchValue"
placeholder="请扫描盘点单号"
prefix-icon="el-icon-search"
@keyup.enter.native="onSearch"
ref="searchInput"
/>
</div>
<!-- 盘点单列表 -->
<div class="content-area">
<div
v-for="item in list"
:key="item.reportId"
class="inbound-card"
@click="goToDetail(item)">
<div class="card-details">
<div class="detail-item">
<div class="detail-label">盘点单号</div>
<div class="detail-value">{{ item.reportId }}</div>
</div>
<div class="detail-item">
<div class="detail-label">标签张数</div>
<div class="detail-value">
<span class="qualified">{{ item.checkedCount }}</span>
<span class="separator">/</span>
<span class="total">{{ item.totalCount }}</span>
</div>
</div>
<div class="detail-item">
<div class="detail-label">物料总数</div>
<div class="detail-value">
<span class="qualified">{{ item.checkedQty }}</span>
<span class="separator">/</span>
<span class="total">{{ item.totalQty }}</span>
</div>
</div>
</div>
<div class="card-footer" v-if="item.statusDesc">
<span class="status-tag" :class="'status-' + item.status">{{ item.statusDesc }}</span>
</div>
</div>
<!-- 空状态 -->
<div v-if="list.length === 0 && !loading" class="empty-state">
<i class="el-icon-box"></i>
<p>暂无盘点单</p>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="loading-state">
<i class="el-icon-loading"></i>
<p>加载中...</p>
</div>
</div>
</div>
</template>
<script>
import { countingWIPTodayList } from '@/api/warehouse/countingWIP.js'
export default {
name: 'CountingList',
data() {
return {
searchValue: '',
list: [],
loading: false
}
},
mounted() {
//
this.$nextTick(() => {
if (this.$refs.searchInput) {
this.$refs.searchInput.focus()
}
})
//
this.getList()
},
methods: {
//
onSearch() {
this.getList()
},
//
getList() {
this.loading = true
const params = {
reportId: this.searchValue
}
countingWIPTodayList(params).then(({ data }) => {
if (data && data.code === 0) {
this.list = data.list || []
} else {
this.$message.error(data.msg || '查询失败')
}
this.loading = false
}).catch(() => {
this.loading = false
this.$message.error('查询失败')
})
},
//
goToDetail(item) {
this.$router.push({
name: 'countingDetail',
query: {
site: item.site,
buNo: item.buNo,
reportId: item.reportId,
totalCount: item.totalCount,
totalQty: item.totalQty,
checkedCount: item.checkedCount,
checkedQty: item.checkedQty
}
})
}
}
}
</script>
<style scoped>
.pda-container {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
background: #f5f5f5;
}
/* 头部栏 */
.header-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 16px;
background: #17B3A3;
color: white;
height: 40px;
min-height: 40px;
}
.header-left {
display: flex;
align-items: center;
cursor: pointer;
font-size: 16px;
font-weight: 500;
}
.header-left i {
margin-right: 8px;
font-size: 18px;
}
.header-right {
cursor: pointer;
font-size: 16px;
font-weight: 500;
}
/* 搜索容器 */
.search-container {
padding: 12px 16px;
background: white;
}
/* 内容区域 */
.content-area {
flex: 1;
overflow-y: auto;
padding: 12px 16px;
}
/* 入库卡片 */
.inbound-card {
background: white;
border-radius: 8px;
margin-bottom: 12px;
padding: 16px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: all 0.2s ease;
}
.inbound-card:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
transform: translateY(-1px);
}
.inbound-card:active {
transform: translateY(0);
}
/* 卡片详情 */
.card-details {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 8px;
padding-bottom: 12px;
}
.detail-item {
flex: 1;
text-align: center;
min-width: 0;
}
.detail-label {
font-size: 12px;
color: #999;
margin-bottom: 6px;
line-height: 1.2;
white-space: nowrap;
}
.detail-value {
font-size: 16px;
color: #333;
line-height: 1.2;
font-weight: 500;
word-break: break-all;
}
.detail-value .qualified {
color: #17B3A3;
font-weight: bold;
}
.detail-value .separator {
color: #333;
margin: 0 2px;
}
.detail-value .total {
color: #333;
font-weight: 500;
}
/* 卡片底部 */
.card-footer {
padding-top: 8px;
border-top: 1px solid #f0f0f0;
text-align: right;
}
.status-tag {
display: inline-block;
padding: 3px 10px;
border-radius: 12px;
font-size: 11px;
color: #fff;
font-weight: 500;
}
.status-0 {
background-color: #909399;
}
.status-1 {
background-color: #409EFF;
}
.status-2 {
background-color: #E6A23C;
}
.status-3 {
background-color: #67C23A;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
color: #999;
}
.empty-state i {
font-size: 48px;
margin-bottom: 16px;
}
.empty-state p {
font-size: 14px;
margin: 0;
}
/* 加载状态 */
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
color: #17B3A3;
}
.loading-state i {
font-size: 24px;
margin-bottom: 12px;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.loading-state p {
font-size: 14px;
margin: 0;
}
/* 响应式设计 */
@media (max-width: 360px) {
.header-bar {
padding: 8px 12px;
}
.search-container {
padding: 8px 12px;
}
.content-area {
padding: 8px 12px;
}
.inbound-card {
padding: 12px;
}
.card-details {
flex-wrap: wrap;
gap: 6px;
}
.detail-item {
flex: 0 0 48%;
margin-bottom: 6px;
min-width: 60px;
}
}
</style>
Loading…
Cancel
Save