src/pages/inventoryManagement/stockManagement/index.vue
@@ -2,39 +2,141 @@
  <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;
@@ -74,12 +176,74 @@
    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;
@@ -101,4 +265,70 @@
  :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>