| | |
| | | <view class="app-container"> |
| | | <PageHeader title="库存管理" |
| | | @back="goBack" /> |
| | | <view v-if="loading" |
| | | class="loading-state"> |
| | | <up-loading-icon text="加载中..."></up-loading-icon> |
| | | <!-- 库存预警开关 --> |
| | | <view class="warn-bar"> |
| | | <view class="warn-left"> |
| | | <text class="warn-label">只看预警</text> |
| | | <up-switch v-model="warnOnly" |
| | | size="small" |
| | | activeColor="#f56c6c" |
| | | @change="handleWarnToggle"></up-switch> |
| | | </view> |
| | | </view> |
| | | <!-- 预警列表 --> |
| | | <template v-if="warnOnly"> |
| | | <view class="warn-search"> |
| | | <view class="search-input"> |
| | | <up-input class="search-text" |
| | | v-model="warnSearch.productName" |
| | | placeholder="请输入产品名称" |
| | | clearable |
| | | @confirm="handleWarnQuery" /> |
| | | </view> |
| | | <view class="filter-button" |
| | | @click="handleWarnQuery"> |
| | | <up-icon name="search" |
| | | size="24" |
| | | color="#999"></up-icon> |
| | | </view> |
| | | </view> |
| | | <scroll-view scroll-y |
| | | class="warn-list" |
| | | v-if="warnList.length > 0" |
| | | @scrolltolower="loadWarnMore"> |
| | | <view v-for="item in warnList" |
| | | :key="item.id" |
| | | class="ledger-item warn-item" |
| | | :class="{ 'is-warn': isWarnRow(item) }"> |
| | | <view class="item-header"> |
| | | <view class="item-left"> |
| | | <view class="document-icon warn-icon"> |
| | | <up-icon name="warning" |
| | | size="16" |
| | | color="#ffffff"></up-icon> |
| | | </view> |
| | | <text class="item-id">{{ item.productName || '-' }}</text> |
| | | </view> |
| | | <up-tag text="预警" |
| | | type="error" |
| | | size="mini" |
| | | v-if="isWarnRow(item)" /> |
| | | </view> |
| | | <up-divider></up-divider> |
| | | <view class="item-details"> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">规格型号</text> |
| | | <text class="detail-value">{{ item.model || '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">单位</text> |
| | | <text class="detail-value">{{ item.unit || '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">数量</text> |
| | | <text class="detail-value">{{ formatQty(item.qualitity) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">锁定数量</text> |
| | | <text class="detail-value">{{ formatQty(item.lockedQuantity) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">可用数量</text> |
| | | <text class="detail-value" |
| | | :class="{ 'warn-value': isWarnRow(item) }">{{ formatQty(item.un_locked_quantity ?? item.unLockedQuantity) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">预警值</text> |
| | | <text class="detail-value">{{ formatQty(item.warnNum) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">批号</text> |
| | | <text class="detail-value">{{ item.batchNo || '-' }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <up-loadmore :status="warnStatus" /> |
| | | </scroll-view> |
| | | <view v-else-if="!warnLoading" |
| | | class="no-data"> |
| | | <up-empty mode="data" |
| | | text="暂无预警库存"></up-empty> |
| | | </view> |
| | | </template> |
| | | <!-- 库存列表 --> |
| | | <template v-else> |
| | | <up-tabs :list="tabs" |
| | | @click="handleTabClick" |
| | | :current="activeTab" /> |
| | | <swiper class="swiper-box" |
| | | :current="activeTab" |
| | | @change="handleSwiperChange"> |
| | | <swiper-item class="swiper-item" |
| | | v-for="tab in products" |
| | | :key="tab.id"> |
| | | <record :product-id="tab.id" |
| | | v-if="activeTab === products.indexOf(tab) || initializedTabs.includes(tab.id)" /> |
| | | </swiper-item> |
| | | </swiper> |
| | | <view v-if="loading" |
| | | class="loading-state"> |
| | | <up-loading-icon text="加载中..."></up-loading-icon> |
| | | </view> |
| | | <template v-else> |
| | | <up-tabs :list="tabs" |
| | | @click="handleTabClick" |
| | | :current="activeTab" /> |
| | | <swiper class="swiper-box" |
| | | :current="activeTab" |
| | | @change="handleSwiperChange"> |
| | | <swiper-item class="swiper-item" |
| | | v-for="tab in products" |
| | | :key="tab.id"> |
| | | <record :product-id="tab.id" |
| | | v-if="activeTab === products.indexOf(tab) || initializedTabs.includes(tab.id)" /> |
| | | </swiper-item> |
| | | </swiper> |
| | | </template> |
| | | </template> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, onMounted } from "vue"; |
| | | import { ref, reactive, onMounted } from "vue"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import Record from "./Record.vue"; |
| | | import { productTreeList } from "@/api/basicData/product.js"; |
| | | import { getStockWarnPage } from "@/api/inventoryManagement/stockInventory.js"; |
| | | |
| | | const activeTab = ref(0); |
| | | const tabs = ref([]); |
| | | const products = ref([]); |
| | | const loading = ref(false); |
| | | const initializedTabs = ref([]); |
| | | |
| | | // 预警 |
| | | const warnOnly = ref(false); |
| | | const warnLoading = ref(false); |
| | | const warnStatus = ref("loadmore"); |
| | | const warnList = ref([]); |
| | | const warnPage = reactive({ current: 1, size: 10, total: 0 }); |
| | | const warnSearch = reactive({ productName: "" }); |
| | | |
| | | const handleTabClick = item => { |
| | | activeTab.value = item.index; |
| | |
| | | uni.navigateBack(); |
| | | }; |
| | | |
| | | // 预警相关 |
| | | const formatQty = val => { |
| | | if (val === null || val === undefined || val === "") return "-"; |
| | | return Number(val) || 0; |
| | | }; |
| | | |
| | | const getAvailable = item => { |
| | | const val = item.un_locked_quantity ?? item.unLockedQuantity; |
| | | return Number(val) || 0; |
| | | }; |
| | | |
| | | // 可用数量 ≤ 预警值 且 预警值>0 |
| | | const isWarnRow = item => { |
| | | const warnNum = Number(item.warnNum) || 0; |
| | | if (warnNum <= 0) return false; |
| | | return getAvailable(item) <= warnNum; |
| | | }; |
| | | |
| | | const getWarnList = () => { |
| | | warnLoading.value = true; |
| | | warnStatus.value = "loading"; |
| | | getStockWarnPage({ |
| | | current: warnPage.current, |
| | | size: warnPage.size, |
| | | productName: warnSearch.productName, |
| | | }) |
| | | .then(res => { |
| | | const records = res?.data?.records || []; |
| | | warnList.value = |
| | | warnPage.current === 1 ? records : [...warnList.value, ...records]; |
| | | warnPage.total = res?.data?.total || 0; |
| | | warnStatus.value = |
| | | warnList.value.length >= warnPage.total ? "nomore" : "loadmore"; |
| | | }) |
| | | .catch(() => { |
| | | warnStatus.value = "loadmore"; |
| | | }) |
| | | .finally(() => { |
| | | warnLoading.value = false; |
| | | }); |
| | | }; |
| | | |
| | | const handleWarnQuery = () => { |
| | | warnPage.current = 1; |
| | | warnList.value = []; |
| | | getWarnList(); |
| | | }; |
| | | |
| | | const loadWarnMore = () => { |
| | | if (warnStatus.value === "nomore" || warnLoading.value) return; |
| | | warnPage.current++; |
| | | getWarnList(); |
| | | }; |
| | | |
| | | const handleWarnToggle = () => { |
| | | if (warnOnly.value && !warnList.value.length) { |
| | | handleWarnQuery(); |
| | | } |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | fetchProducts(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/styles/procurement-common.scss"; |
| | | |
| | | .app-container { |
| | | display: flex; |
| | | flex-direction: column; |
| | |
| | | :deep(.up-tabs) { |
| | | background-color: #fff; |
| | | } |
| | | |
| | | .warn-bar { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | background: #fff; |
| | | padding: 16rpx 30rpx; |
| | | |
| | | .warn-left { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 12rpx; |
| | | } |
| | | |
| | | .warn-label { |
| | | font-size: 28rpx; |
| | | color: #333; |
| | | } |
| | | } |
| | | |
| | | .warn-search { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 12rpx; |
| | | background: #fff; |
| | | padding: 16rpx 30rpx; |
| | | |
| | | .search-input { |
| | | flex: 1; |
| | | background: #f5f5f5; |
| | | border-radius: 40rpx; |
| | | padding: 0 24rpx; |
| | | height: 72rpx; |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | } |
| | | |
| | | .warn-list { |
| | | flex: 1; |
| | | height: 0; |
| | | padding: 20rpx; |
| | | box-sizing: border-box; |
| | | } |
| | | |
| | | .warn-item { |
| | | padding: 24rpx; |
| | | |
| | | &.is-warn { |
| | | border: 2rpx solid #f56c6c; |
| | | } |
| | | |
| | | .warn-icon { |
| | | background: linear-gradient(135deg, #f56c6c, #c0392b); |
| | | } |
| | | |
| | | .warn-value { |
| | | color: #f56c6c; |
| | | font-weight: 600; |
| | | } |
| | | } |
| | | |
| | | .no-data { |
| | | flex: 1; |
| | | padding-top: 150rpx; |
| | | } |
| | | </style> |