<template>
|
<view class="stock-mgmt-page">
|
<PageHeader title="库存管理" @back="goBack" />
|
|
<!-- 标签:合格库存 / 不合格库存 -->
|
<view class="tabs-wrap">
|
<view
|
v-for="tab in tabs"
|
:key="tab.name"
|
class="tab-item"
|
:class="{ active: activeTab === tab.name }"
|
@click="activeTab = tab.name"
|
>
|
<text>{{ tab.label }}</text>
|
</view>
|
</view>
|
|
<!-- 搜索区域 -->
|
<view class="search-section">
|
<view class="search-row">
|
<view class="search-input-wrap">
|
<up-input
|
v-model="searchForm.productName"
|
placeholder="产品大类"
|
clearable
|
/>
|
</view>
|
<view class="btn-search" @click="handleQuery">
|
<view class="btn-search-inner">
|
<up-icon name="search" size="22" color="#fff"></up-icon>
|
<text>搜索</text>
|
</view>
|
</view>
|
</view>
|
</view>
|
|
<!-- 列表 + 滚动分页 -->
|
<view class="list-section">
|
<view v-if="tableData.length > 0">
|
<view
|
v-for="(item, index) in tableData"
|
:key="item.id || index"
|
class="card-item"
|
>
|
<view class="card-click" @click="goDetail(item)">
|
<view class="card-header">
|
<view class="header-main">
|
<text class="product-name">{{ item.productName }}</text>
|
<text class="sub-title">{{ item.model || item.updateTime }}</text>
|
</view>
|
</view>
|
<up-divider />
|
<view class="card-body">
|
<view class="row"><text class="l">规格型号</text><text class="r">{{ item.model }}</text></view>
|
<view class="row"><text class="l">单位</text><text class="r">{{ item.unit }}</text></view>
|
<view class="row"><text class="l">总库存数</text><text class="r highlight">{{ item.qualitity }}</text></view>
|
<view class="row"><text class="l">冻结数量</text><text class="r">{{ item.lockedQuantity || 0 }}</text></view>
|
<view class="row"><text class="l">可用库存</text><text class="r">{{ item.unLockedQuantity ?? (item.qualitity - (item.lockedQuantity || 0)) }}</text></view>
|
<view class="row"><text class="l">最近更新时间</text><text class="r">{{ item.updateTime }}</text></view>
|
</view>
|
</view>
|
<view class="card-actions">
|
<view
|
class="btn-link btn-link-primary"
|
:class="{ disabled: !(item.unLockedQuantity > 0) }"
|
@click="openSubtract(item)"
|
>出库</view>
|
<view
|
class="btn-link btn-link-warn"
|
v-if="item.unLockedQuantity > 0"
|
@click="openFrozen(item)"
|
>冻结</view>
|
<view
|
class="btn-link btn-link-plain"
|
v-if="(item.lockedQuantity || 0) > 0"
|
@click="openThaw(item)"
|
>解冻</view>
|
</view>
|
</view>
|
<view class="load-more-wrap">
|
<u-loadmore :status="loadStatus" @loadmore="loadMore" />
|
</view>
|
</view>
|
<view v-else class="no-data">暂无数据</view>
|
</view>
|
|
<!-- 出库/冻结/解冻弹窗 -->
|
<up-popup :show="showQuantityPopup" mode="center" round="16" @close="closeQuantityPopup">
|
<view class="popup-inner">
|
<view class="popup-title">{{ quantityTitle }}</view>
|
<view class="form-row">
|
<text class="form-label">数量</text>
|
<up-input v-model="quantityForm.num" type="number" :placeholder="'最大' + maxQuantity" />
|
</view>
|
<view class="popup-footer">
|
<view class="btn-cancel" @click="closeQuantityPopup">取消</view>
|
<view class="btn-ok" @click="submitQuantity">确定</view>
|
</view>
|
</view>
|
</up-popup>
|
|
<!-- 右下角新增按钮:进入新增库存页面 -->
|
<view class="fab-button" @click="goAdd">
|
<up-icon name="plus" size="24" color="#ffffff"></up-icon>
|
</view>
|
</view>
|
</template>
|
|
<script setup>
|
import { ref, reactive, toRefs, watch, computed } from 'vue'
|
import { onShow, onReachBottom } from '@dcloudio/uni-app'
|
import PageHeader from '@/components/PageHeader.vue'
|
import {
|
getStockInventoryListPage,
|
createStockInventory,
|
subtractStockInventory,
|
frozenStockInventory,
|
thawStockInventory
|
} from '@/api/inventoryManagement/stockInventory.js'
|
import {
|
getStockUninventoryListPage,
|
createStockUnInventory,
|
subtractStockUnInventory,
|
frozenStockUninventory,
|
thawStockUninventory
|
} from '@/api/inventoryManagement/stockUninventory.js'
|
|
const activeTab = ref('qualified')
|
const tabs = [
|
{ label: '合格库存', name: 'qualified' },
|
{ label: '不合格库存', name: 'unqualified' }
|
]
|
const tableData = ref([])
|
const total = ref(0)
|
const loadStatus = ref('loadmore') // loadmore | loading | nomore | error
|
const showQuantityPopup = ref(false)
|
const quantityOp = ref('') // subtract | frozen | thaw
|
const currentRecord = ref(null)
|
const page = reactive({ current: 1, size: 20 })
|
const data = reactive({
|
searchForm: { productName: '' },
|
quantityForm: { num: '' }
|
})
|
const { searchForm, quantityForm } = toRefs(data)
|
|
const isQualified = () => activeTab.value === 'qualified'
|
const getList = () => {
|
const isFirstPage = page.current === 1
|
if (isFirstPage) {
|
uni.showLoading({ title: '加载中...', mask: true })
|
}
|
const params = { ...page, productName: searchForm.value.productName }
|
const api = isQualified() ? getStockInventoryListPage : getStockUninventoryListPage
|
api(params)
|
.then(res => {
|
uni.hideLoading()
|
const records = res.data?.records || []
|
const totalCount = res.data?.total || 0
|
if (isFirstPage) {
|
tableData.value = records
|
} else {
|
tableData.value = [...tableData.value, ...records]
|
}
|
total.value = totalCount
|
if (tableData.value.length >= totalCount || totalCount === 0) {
|
loadStatus.value = 'nomore'
|
} else {
|
loadStatus.value = 'loadmore'
|
}
|
})
|
.catch(() => {
|
uni.hideLoading()
|
loadStatus.value = 'error'
|
if (isFirstPage) {
|
uni.showToast({ title: '加载失败', icon: 'none' })
|
}
|
})
|
}
|
|
const loadMore = () => {
|
if (loadStatus.value === 'nomore' || loadStatus.value === 'loading') return
|
loadStatus.value = 'loading'
|
page.current++
|
getList()
|
}
|
|
watch(activeTab, () => {
|
page.current = 1
|
loadStatus.value = 'loadmore'
|
getList()
|
})
|
|
const handleQuery = () => {
|
page.current = 1
|
loadStatus.value = 'loadmore'
|
getList()
|
}
|
|
const goAdd = () => {
|
const type = isQualified() ? '0' : '1'
|
uni.navigateTo({
|
url: `/pages/inventoryManagement/stockManagement/add?type=${type}`
|
})
|
}
|
|
const quantityTitle = computed(() => {
|
if (quantityOp.value === 'frozen') return '冻结库存'
|
if (quantityOp.value === 'thaw') return '解冻库存'
|
return ''
|
})
|
const maxQuantity = computed(() => {
|
if (!currentRecord.value) return 0
|
if (quantityOp.value === 'frozen') return currentRecord.value.unLockedQuantity || 0
|
if (quantityOp.value === 'thaw') return currentRecord.value.lockedQuantity || 0
|
return 0
|
})
|
|
const openSubtract = (row) => {
|
if (!(row.unLockedQuantity > 0)) return
|
try {
|
uni.setStorageSync('stockSubtractRecord', JSON.stringify({
|
item: row,
|
type: isQualified() ? '0' : '1'
|
}))
|
} catch (e) {}
|
const typeParam = isQualified() ? '0' : '1'
|
uni.navigateTo({
|
url: `/pages/inventoryManagement/stockManagement/subtract?type=${typeParam}&id=${row.id}`
|
})
|
}
|
const openFrozen = (row) => {
|
quantityOp.value = 'frozen'
|
currentRecord.value = row
|
quantityForm.value.num = ''
|
showQuantityPopup.value = true
|
}
|
const openThaw = (row) => {
|
quantityOp.value = 'thaw'
|
currentRecord.value = row
|
quantityForm.value.num = ''
|
showQuantityPopup.value = true
|
}
|
const closeQuantityPopup = () => {
|
showQuantityPopup.value = false
|
currentRecord.value = null
|
quantityOp.value = ''
|
}
|
const submitQuantity = () => {
|
const num = Number(quantityForm.value.num)
|
if (!num || num <= 0 || num > maxQuantity.value) {
|
uni.showToast({ title: `请输入 1~${maxQuantity.value} 之间的数量`, icon: 'none' })
|
return
|
}
|
const id = currentRecord.value?.id
|
if (!id) return
|
const base = { id, lockedQuantity: num }
|
let promise
|
if (quantityOp.value === 'frozen') {
|
promise = isQualified() ? frozenStockInventory(base) : frozenStockUninventory(base)
|
} else {
|
promise = isQualified() ? thawStockInventory(base) : thawStockUninventory(base)
|
}
|
promise.then(() => {
|
uni.showToast({ title: '操作成功', icon: 'success' })
|
closeQuantityPopup()
|
getList()
|
}).catch(() => uni.showToast({ title: '操作失败', icon: 'none' }))
|
}
|
|
const goDetail = (item) => {
|
if (!item) return
|
try {
|
uni.setStorageSync('stockDetailItem', JSON.stringify({
|
item,
|
type: isQualified() ? '0' : '1'
|
}))
|
} catch (e) {}
|
if (!item.id) {
|
uni.navigateTo({ url: '/pages/inventoryManagement/stockManagement/view' })
|
} else {
|
uni.navigateTo({ url: '/pages/inventoryManagement/stockManagement/view?id=' + item.id })
|
}
|
}
|
|
const goBack = () => uni.navigateBack()
|
onShow(() => getList())
|
onReachBottom(() => {
|
loadMore()
|
})
|
</script>
|
|
<style lang="scss" scoped>
|
.stock-mgmt-page {
|
min-height: 100vh;
|
background: #f5f5f5;
|
padding-bottom: 120rpx;
|
}
|
.tabs-wrap {
|
display: flex;
|
background: #fff;
|
padding: 24rpx;
|
gap: 24rpx;
|
}
|
.tab-item {
|
flex: 1;
|
text-align: center;
|
padding: 20rpx;
|
border-radius: 12rpx;
|
background: #f0f0f0;
|
font-size: 28rpx;
|
color: #666;
|
}
|
.tab-item.active {
|
background: #2979ff;
|
color: #fff;
|
}
|
.search-section {
|
background: #fff;
|
margin: 24rpx;
|
padding: 24rpx;
|
border-radius: 16rpx;
|
}
|
.search-row {
|
display: flex;
|
align-items: center;
|
}
|
.search-input-wrap { flex: 1; margin-right: 20rpx; min-width: 0; }
|
.btn-search {
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
width: 180rpx;
|
min-height: 72rpx;
|
flex-shrink: 0;
|
padding: 20rpx 24rpx;
|
background: #2979ff;
|
color: #fff;
|
border-radius: 12rpx;
|
font-size: 28rpx;
|
box-sizing: border-box;
|
text-align: center;
|
}
|
.btn-search-inner {
|
display: flex;
|
flex-direction: row;
|
align-items: center;
|
justify-content: center;
|
gap: 8rpx;
|
}
|
.btn-search text { line-height: 1; vertical-align: middle; }
|
:deep(.btn-search-inner > *),
|
:deep(.btn-search > *) {
|
flex-shrink: 0;
|
display: inline-flex;
|
align-items: center;
|
justify-content: center;
|
}
|
.list-section { padding: 0 24rpx; }
|
.card-item {
|
background: #fff;
|
border-radius: 16rpx;
|
padding: 24rpx;
|
margin-bottom: 24rpx;
|
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
|
}
|
.card-header { padding: 8rpx 0; }
|
.header-main {
|
display: flex;
|
flex-direction: column;
|
gap: 8rpx;
|
}
|
.product-name { font-size: 30rpx; font-weight: 500; color: #333; }
|
.sub-title { font-size: 24rpx; color: #999; }
|
.card-body .row {
|
display: flex;
|
justify-content: space-between;
|
padding: 12rpx 0;
|
font-size: 26rpx;
|
}
|
.card-body .l { color: #666; }
|
.card-body .r { color: #333; }
|
.card-body .r.highlight { color: #2979ff; font-weight: 500; }
|
.card-actions {
|
display: flex;
|
gap: 16rpx;
|
margin-top: 16rpx;
|
padding-top: 16rpx;
|
border-top: 1rpx solid #eee;
|
justify-content: flex-end;
|
}
|
.btn-link {
|
min-width: 120rpx;
|
padding: 10rpx 20rpx;
|
border-radius: 24rpx;
|
font-size: 26rpx;
|
text-align: center;
|
border-width: 1rpx;
|
border-style: solid;
|
}
|
.btn-link-primary {
|
color: #ffffff;
|
background: #2979ff;
|
border-color: #2979ff;
|
}
|
.btn-link-warn {
|
color: #ff9f1a;
|
background: rgba(255, 159, 26, 0.08);
|
border-color: rgba(255, 159, 26, 0.6);
|
}
|
.btn-link-plain {
|
color: #666666;
|
background: #f5f5f5;
|
border-color: #e0e0e0;
|
}
|
.btn-link.disabled {
|
color: #cccccc;
|
background: #f5f5f5;
|
border-color: #e0e0e0;
|
}
|
.no-data { text-align: center; padding: 80rpx 0; color: #999; font-size: 28rpx; }
|
|
/* 右下角浮动新增按钮 */
|
.fab-button {
|
position: fixed;
|
bottom: calc(30px + env(safe-area-inset-bottom));
|
right: 30px;
|
width: 56px;
|
height: 56px;
|
background: #2979ff;
|
border-radius: 50%;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
box-shadow: 0 4px 16px rgba(41, 121, 255, 0.3);
|
z-index: 1000;
|
}
|
|
.popup-inner { padding: 40rpx; min-width: 560rpx; background: #fff; }
|
.popup-title { font-size: 32rpx; font-weight: 500; margin-bottom: 32rpx; }
|
.form-row { margin-bottom: 24rpx; }
|
.form-row .form-label { display: block; font-size: 26rpx; color: #666; margin-bottom: 12rpx; }
|
.date-picker-trigger {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
padding: 20rpx 24rpx;
|
background: #f5f5f5;
|
border-radius: 12rpx;
|
font-size: 28rpx;
|
}
|
.date-picker-text { color: #333; }
|
.date-picker-text.placeholder { color: #999; }
|
.popup-footer { display: flex; gap: 24rpx; margin-top: 40rpx; }
|
.btn-cancel { flex: 1; text-align: center; padding: 24rpx; background: #f0f0f0; border-radius: 12rpx; }
|
.btn-ok { flex: 1; text-align: center; padding: 24rpx; background: #2979ff; color: #fff; border-radius: 12rpx; }
|
</style>
|