zhangwencui
昨天 95fce9ecb77e9615df925eee143ce34647c694ce
扫码出库和扫码入库功能开发
已添加2个文件
已修改3个文件
1341 ■■■■ 文件已修改
src/pages.json 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inventoryManagement/receiptManagement/index.vue 593 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inventoryManagement/scanIn/index.vue 307 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inventoryManagement/scanOut/index.vue 339 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/works.vue 86 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages.json
@@ -747,6 +747,20 @@
      }
    },
    {
      "path": "pages/inventoryManagement/scanIn/index",
      "style": {
        "navigationBarTitleText": "扫码入库",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/inventoryManagement/scanOut/index",
      "style": {
        "navigationBarTitleText": "扫码出库",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/safeProduction/safeQualifications/index",
      "style": {
        "navigationBarTitleText": "规程与资质",
@@ -1337,4 +1351,4 @@
    "navigationBarTitleText": "RuoYi",
    "navigationBarBackgroundColor": "#FFFFFF"
  }
}
}
src/pages/inventoryManagement/receiptManagement/index.vue
@@ -1,34 +1,42 @@
<template>
  <view class="stock-in-page">
    <PageHeader title="自定义入库" @back="goBack" />
    <PageHeader title="自定义入库"
                @back="goBack" />
    <!-- æœç´¢åŒºåŸŸ -->
    <view class="search-section">
      <view class="search-bar">
        <view class="search-input">
          <up-input
            v-model="searchForm.supplierName"
            placeholder="请输入供应商名称"
            clearable
          />
          <up-input v-model="searchForm.supplierName"
                    placeholder="请输入供应商名称"
                    clearable />
        </view>
        <view class="search-button" @click="handleQuery">
          <up-icon name="search" size="24" color="#999"></up-icon>
        <view class="search-button"
              @click="handleQuery">
          <up-icon name="search"
                   size="24"
                   color="#999"></up-icon>
        </view>
      </view>
      <view class="date-filter" @click="openDatePickerHandler">
      <view class="date-filter"
            @click="openDatePickerHandler">
        <text class="date-text">{{ searchForm.timeStr || '选择日期' }}</text>
        <up-icon name="calendar" size="18" color="#999"></up-icon>
        <up-icon name="calendar"
                 size="18"
                 color="#999"></up-icon>
      </view>
    </view>
    <!-- åˆ—表 -->
    <view class="stock-list" v-if="tableData.length > 0">
      <view v-for="(item, index) in tableData" :key="index" class="stock-item">
    <view class="stock-list"
          v-if="tableData.length > 0">
      <view v-for="(item, index) in tableData"
            :key="index"
            class="stock-item">
        <view class="item-header">
          <view class="item-left">
            <view class="batch-icon">
              <up-icon name="file-text" size="16" color="#ffffff"></up-icon>
              <up-icon name="file-text"
                       size="16"
                       color="#ffffff"></up-icon>
            </view>
            <text class="batch-text">{{ item.inboundBatches }}</text>
          </view>
@@ -37,7 +45,6 @@
          </view>
        </view>
        <up-divider></up-divider>
        <view class="item-details">
          <view class="detail-row">
            <text class="detail-label">供应商名称</text>
@@ -76,331 +83,341 @@
            <text class="detail-value">{{ item.createBy }}</text>
          </view>
        </view>
        <view class="item-actions">
          <u-button type="primary" size="small" @click="handleEdit(item)">编辑</u-button>
          <u-button type="error" size="small" plain @click="handleDeleteSingle(item)">删除</u-button>
          <u-button type="primary"
                    size="small"
                    @click="handleEdit(item)">编辑</u-button>
          <u-button type="error"
                    size="small"
                    plain
                    @click="handleDeleteSingle(item)">删除</u-button>
        </view>
      </view>
    </view>
    <view v-else class="no-data">
    <view v-else
          class="no-data">
      <text>暂无数据</text>
    </view>
    <!-- æµ®åŠ¨æ“ä½œæŒ‰é’® -->
    <view class="fab-button" @click="handleAdd">
      <up-icon name="plus" size="24" color="#ffffff"></up-icon>
    <view class="fab-button"
          @click="handleAdd">
      <up-icon name="plus"
               size="24"
               color="#ffffff"></up-icon>
    </view>
    <!-- æ—¥æœŸé€‰æ‹©å™¨ -->
    <up-popup :show="showDatePicker" mode="bottom" @close="showDatePicker = false">
      <up-datetime-picker
        :show="true"
        v-model="dateValue"
        @confirm="onDateConfirm"
        @cancel="showDatePicker = false"
        mode="date"
      />
    <up-popup :show="showDatePicker"
              mode="bottom"
              @close="showDatePicker = false">
      <up-datetime-picker :show="true"
                          v-model="dateValue"
                          @confirm="onDateConfirm"
                          @cancel="showDatePicker = false"
                          mode="date" />
    </up-popup>
    <!-- è¡¨å•弹窗 -->
    <form-dia-manual ref="formDiaManual" @close="getList" @success="getList" />
    <form-dia-manual ref="formDiaManual"
                     @close="getList"
                     @success="getList" />
  </view>
</template>
<script setup>
import { ref, reactive, toRefs, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import dayjs from 'dayjs'
import PageHeader from '@/components/PageHeader.vue'
import FormDiaManual from './components/formDiaManual.vue'
import useUserStore from '@/store/modules/user'
import { formatDateToYMD } from '@/utils/ruoyi'
import {
  getInPageByCustom,
  delStockInCustom
} from "@/api/inventoryManagement/stockIn.js"
  import { ref, reactive, toRefs, onMounted } from "vue";
  import { onShow } from "@dcloudio/uni-app";
  import dayjs from "dayjs";
  import PageHeader from "@/components/PageHeader.vue";
  import FormDiaManual from "./components/formDiaManual.vue";
  import useUserStore from "@/store/modules/user";
  import { formatDateToYMD } from "@/utils/ruoyi";
  import {
    getInPageByCustom,
    delStockInCustom,
  } from "@/api/inventoryManagement/stockIn.js";
const userStore = useUserStore()
  const userStore = useUserStore();
const tableData = ref([])
const showDatePicker = ref(false)
const dateValue = ref(new Date().getTime())
const formDiaManual = ref(null)
  const tableData = ref([]);
  const showDatePicker = ref(false);
  const dateValue = ref(new Date().getTime());
  const formDiaManual = ref(null);
const page = reactive({
  current: 1,
  size: 20,
})
const total = ref(0)
  const page = reactive({
    current: 1,
    size: 20,
  });
  const total = ref(0);
const data = reactive({
  searchForm: {
    supplierName: '',
    timeStr: '',
  },
})
const { searchForm } = toRefs(data)
  const data = reactive({
    searchForm: {
      supplierName: "",
      timeStr: "",
    },
  });
  const { searchForm } = toRefs(data);
// ç»Ÿä¸€ç”¨ dayjs è¾“出 YYYY-MM-DD
const formatYMDLocal = (ts) => dayjs(Number(ts)).format('YYYY-MM-DD')
  // ç»Ÿä¸€ç”¨ dayjs è¾“出 YYYY-MM-DD
  const formatYMDLocal = ts => dayjs(Number(ts)).format("YYYY-MM-DD");
// è¿”回上一页
const goBack = () => {
  uni.navigateBack()
}
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
// æŸ¥è¯¢åˆ—表
const handleQuery = () => {
  page.current = 1
  getList()
}
  // æŸ¥è¯¢åˆ—表
  const handleQuery = () => {
    page.current = 1;
    getList();
  };
const getList = () => {
  uni.showLoading({
    title: '加载中...',
    mask: true
  })
  const params = {
    ...page,
    supplierName: searchForm.value.supplierName,
    timeStr: searchForm.value.timeStr
  }
  getInPageByCustom(params).then(res => {
    uni.hideLoading()
    tableData.value = res.data.records || []
    total.value = res.data.total || 0
  }).catch(() => {
    uni.hideLoading()
    uni.showToast({
      title: '加载失败',
      icon: 'none'
    })
  })
}
  const getList = () => {
    uni.showLoading({
      title: "加载中...",
      mask: true,
    });
// æ‰“开日期选择器(简单可靠)
const openDatePickerHandler = () => {
  // è‹¥å·²æœ‰é€‰ä¸­æ—¥æœŸï¼Œç”¨å®ƒåˆå§‹åŒ–;否则用今天
  dateValue.value = searchForm.value.timeStr
    ? dayjs(searchForm.value.timeStr, 'YYYY-MM-DD').valueOf()
    : Date.now()
  showDatePicker.value = true
}
    const params = {
      ...page,
      supplierName: searchForm.value.supplierName,
      timeStr: searchForm.value.timeStr,
    };
// æ—¥æœŸé€‰æ‹©ç¡®è®¤ï¼ˆä¸Žå…¶ä»–页一致:拿时间戳 -> YYYY-MM-DD)
const onDateConfirm = (e) => {
  searchForm.value.timeStr = formatDateToYMD(e.value)
  showDatePicker.value = false
  handleQuery()
}
    getInPageByCustom(params)
      .then(res => {
        uni.hideLoading();
        tableData.value = res.data.records || [];
        total.value = res.data.total || 0;
      })
      .catch(() => {
        uni.hideLoading();
        uni.showToast({
          title: "加载失败",
          icon: "none",
        });
      });
  };
// æ–°å¢žå…¥åº“
const handleAdd = () => {
  formDiaManual.value?.openDialog('add')
}
  // æ‰“开日期选择器(简单可靠)
  const openDatePickerHandler = () => {
    // è‹¥å·²æœ‰é€‰ä¸­æ—¥æœŸï¼Œç”¨å®ƒåˆå§‹åŒ–;否则用今天
    dateValue.value = searchForm.value.timeStr
      ? dayjs(searchForm.value.timeStr, "YYYY-MM-DD").valueOf()
      : Date.now();
    showDatePicker.value = true;
  };
// ç¼–辑
const handleEdit = (item) => {
  formDiaManual.value?.openDialog('edit', item)
}
  // æ—¥æœŸé€‰æ‹©ç¡®è®¤ï¼ˆä¸Žå…¶ä»–页一致:拿时间戳 -> YYYY-MM-DD)
  const onDateConfirm = e => {
    searchForm.value.timeStr = formatDateToYMD(e.value);
    showDatePicker.value = false;
    handleQuery();
  };
// åˆ é™¤å•条
const handleDeleteSingle = (item) => {
  // æ£€æŸ¥æ˜¯å¦æ˜¯æœ¬äººåˆ›å»º
  if (item.createBy !== userStore.nickName) {
    uni.showToast({
      title: '不可删除他人维护的数据',
      icon: 'none'
    })
    return
  }
  uni.showModal({
    title: '删除',
    content: '确认删除该入库记录吗?',
    success: (res) => {
      if (res.confirm) {
        delStockInCustom({ ids: [item.id] }).then(() => {
          uni.showToast({
            title: '删除成功',
            icon: 'success'
          })
          getList()
        }).catch(() => {
          uni.showToast({
            title: '删除失败',
            icon: 'none'
          })
        })
      }
  // æ–°å¢žå…¥åº“
  const handleAdd = () => {
    formDiaManual.value?.openDialog("add");
  };
  // ç¼–辑
  const handleEdit = item => {
    formDiaManual.value?.openDialog("edit", item);
  };
  // åˆ é™¤å•条
  const handleDeleteSingle = item => {
    // æ£€æŸ¥æ˜¯å¦æ˜¯æœ¬äººåˆ›å»º
    if (item.createBy !== userStore.nickName) {
      uni.showToast({
        title: "不可删除他人维护的数据",
        icon: "none",
      });
      return;
    }
  })
}
onShow(() => {
  getList()
})
    uni.showModal({
      title: "删除",
      content: "确认删除该入库记录吗?",
      success: res => {
        if (res.confirm) {
          delStockInCustom({ ids: [item.id] })
            .then(() => {
              uni.showToast({
                title: "删除成功",
                icon: "success",
              });
              getList();
            })
            .catch(() => {
              uni.showToast({
                title: "删除失败",
                icon: "none",
              });
            });
        }
      },
    });
  };
  onShow(() => {
    getList();
  });
</script>
<style scoped lang="scss">
.stock-in-page {
  min-height: 100vh;
  background: #f5f5f5;
  padding-bottom: 80px;
}
  .stock-in-page {
    min-height: 100vh;
    background: #f5f5f5;
    padding-bottom: 80px;
  }
.search-section {
  background: #fff;
  padding: 16px;
  margin-bottom: 12px;
}
  .search-section {
    background: #fff;
    padding: 16px;
    margin-bottom: 12px;
  }
.search-bar {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 12px;
}
  .search-bar {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-bottom: 12px;
  }
.search-input {
  flex: 1;
}
  .search-input {
    flex: 1;
  }
.search-button {
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #f5f5f5;
  border-radius: 8px;
}
  .search-button {
    width: 44px;
    height: 44px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: #f5f5f5;
    border-radius: 8px;
  }
.date-filter {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 16px;
  background: #f5f5f5;
  border-radius: 8px;
}
  .date-filter {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 12px 16px;
    background: #f5f5f5;
    border-radius: 8px;
  }
.date-text {
  font-size: 14px;
  color: #666;
}
  .date-text {
    font-size: 14px;
    color: #666;
  }
.stock-list {
  padding: 0 16px;
}
  .stock-list {
    padding: 0 16px;
  }
.stock-item {
  background: #fff;
  border-radius: 12px;
  padding: 16px;
  margin-bottom: 12px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
  .stock-item {
    background: #fff;
    border-radius: 12px;
    padding: 16px;
    margin-bottom: 12px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
  }
.item-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 12px;
}
  .item-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 12px;
  }
.item-left {
  display: flex;
  align-items: center;
  gap: 8px;
}
  .item-left {
    display: flex;
    align-items: center;
    gap: 8px;
  }
.batch-icon {
  width: 32px;
  height: 32px;
  background: #2979ff;
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
}
  .batch-icon {
    width: 32px;
    height: 32px;
    background: #2979ff;
    border-radius: 8px;
    display: flex;
    align-items: center;
    justify-content: center;
  }
.batch-text {
  font-size: 14px;
  font-weight: 500;
  color: #333;
}
  .batch-text {
    font-size: 14px;
    font-weight: 500;
    color: #333;
  }
.time-text {
  font-size: 12px;
  color: #999;
}
  .time-text {
    font-size: 12px;
    color: #999;
  }
.item-details {
  margin: 12px 0;
}
  .item-details {
    margin: 12px 0;
  }
.detail-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 8px 0;
}
  .detail-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 8px 0;
  }
.detail-label {
  font-size: 14px;
  color: #666;
}
  .detail-label {
    font-size: 14px;
    color: #666;
  }
.detail-value {
  font-size: 14px;
  color: #333;
  text-align: right;
  flex: 1;
  margin-left: 12px;
}
  .detail-value {
    font-size: 14px;
    color: #333;
    text-align: right;
    flex: 1;
    margin-left: 12px;
  }
.detail-value.highlight {
  color: #2979ff;
  font-weight: 500;
}
  .detail-value.highlight {
    color: #2979ff;
    font-weight: 500;
  }
.detail-value.price {
  color: #ff6b00;
  font-weight: 500;
}
  .detail-value.price {
    color: #ff6b00;
    font-weight: 500;
  }
.item-actions {
  display: flex;
  gap: 12px;
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid #f5f5f5;
}
  .item-actions {
    display: flex;
    gap: 12px;
    margin-top: 12px;
    padding-top: 12px;
    border-top: 1px solid #f5f5f5;
  }
.no-data {
  text-align: center;
  padding: 60px 0;
  color: #999;
  font-size: 14px;
}
  .no-data {
    text-align: center;
    padding: 60px 0;
    color: #999;
    font-size: 14px;
  }
.fab-button {
  position: fixed;
  right: 20px;
  bottom: 80px;
  width: 56px;
  height: 56px;
  background: #2979ff;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 4px 12px rgba(41, 121, 255, 0.4);
  z-index: 999;
}
  .fab-button {
    position: fixed;
    right: 20px;
    bottom: 80px;
    width: 56px;
    height: 56px;
    background: #2979ff;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: 0 4px 12px rgba(41, 121, 255, 0.4);
    z-index: 999;
  }
</style>
src/pages/inventoryManagement/scanIn/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,307 @@
<template>
  <view class="scan-container">
    <PageHeader title="扫码入库"
                @back="goBack" />
    <view class="module-selector"
          v-if="!showForm">
      <view class="module-card"
            @click="startScan('qualified')">
        <view class="module-icon qualified">
          <u-icon name="checkbox-mark"
                  color="#fff"
                  size="40"></u-icon>
        </view>
        <view class="module-info">
          <text class="module-label">合格入库</text>
          <text class="module-desc">扫描合格产品信息</text>
        </view>
      </view>
      <view class="module-card"
            @click="startScan('unqualified')">
        <view class="module-icon unqualified">
          <u-icon name="close"
                  color="#fff"
                  size="40"></u-icon>
        </view>
        <view class="module-info">
          <text class="module-label">不合格入库</text>
          <text class="module-desc">录入不合格品记录</text>
        </view>
      </view>
    </view>
    <view class="form-content"
          v-if="showForm">
      <u-form ref="formRef"
              :model="form"
              :rules="formRules"
              label-width="100px">
        <u-form-item label="入库类型"
                     border-bottom>
          <u-tag :text="type === 'qualified' ? '合格入库' : '不合格入库'"
                 :type="type === 'qualified' ? 'success' : 'error'"></u-tag>
        </u-form-item>
        <u-form-item label="产品名称"
                     border-bottom>
          <u-input v-model="form.productName"
                   readonly
                   border="none"></u-input>
        </u-form-item>
        <u-form-item label="规格型号"
                     border-bottom>
          <u-input v-model="form.productModelName"
                   readonly
                   border="none"></u-input>
        </u-form-item>
        <u-form-item label="单位"
                     border-bottom>
          <u-input v-model="form.unit"
                   readonly
                   border="none"></u-input>
        </u-form-item>
        <u-form-item label="入库数量"
                     prop="qualitity"
                     required
                     border-bottom>
          <u-number-box v-model="form.qualitity"
                        :min="1"
                        :step="1"></u-number-box>
        </u-form-item>
        <u-form-item label="预警数量"
                     prop="warnNum"
                     v-if="type === 'qualified'"
                     border-bottom>
          <u-number-box v-model="form.warnNum"
                        :min="0"
                        :step="1"></u-number-box>
        </u-form-item>
        <u-form-item label="备注"
                     prop="remark"
                     border-bottom>
          <u-textarea v-model="form.remark"
                      placeholder="请输入备注"
                      count></u-textarea>
        </u-form-item>
      </u-form>
      <view class="footer-btns">
        <u-button class="cancel-btn"
                  @click="cancelForm">取消</u-button>
        <u-button class="save-btn"
                  @click="handleSubmit"
                  :loading="loading">确认入库</u-button>
      </view>
    </view>
  </view>
</template>
<script setup>
  import { ref, reactive } from "vue";
  import PageHeader from "@/components/PageHeader.vue";
  import {
    createStockInventory,
    getStockInventoryListPage,
  } from "@/api/inventoryManagement/stockInventory.js";
  import {
    createStockUnInventory,
    getStockUninventoryListPage,
  } from "@/api/inventoryManagement/stockUninventory.js";
  import modal from "@/plugins/modal";
  const showForm = ref(false);
  const type = ref("qualified"); // qualified | unqualified
  const loading = ref(false);
  const formRef = ref(null);
  const form = ref({
    productId: undefined,
    productModelId: undefined,
    productName: "",
    productModelName: "",
    unit: "",
    qualitity: 1,
    warnNum: 0,
    remark: "",
  });
  const formRules = {
    qualitity: [
      {
        required: true,
        type: "number",
        message: "请输入入库数量",
        trigger: ["blur", "change"],
      },
    ],
  };
  const goBack = () => {
    if (showForm.value) {
      showForm.value = false;
    } else {
      uni.navigateBack();
    }
  };
  const cancelForm = () => {
    showForm.value = false;
  };
  const startScan = scanType => {
    type.value = scanType;
    uni.scanCode({
      success: res => {
        handleScanResult(res.result);
      },
      fail: err => {
        modal.msgError("扫码失败");
      },
    });
  };
  const handleScanResult = async result => {
    try {
      // è§£æžäºŒç»´ç æ•°æ®
      const scanData = JSON.parse(result);
      if (!scanData.id) {
        modal.msgError("无效的二维码数据");
        return;
      }
      // ç›´æŽ¥ä»ŽäºŒç»´ç ä¿¡æ¯ä¸­èŽ·å–äº§å“è¯¦æƒ…
      form.value.productId = scanData.productId; // å¦‚果二维码中有 productId
      form.value.productName = scanData.productName;
      form.value.productModelId = scanData.id; // äºŒç»´ç ä¸­çš„ id æ˜¯äº§å“åž‹å· ID
      form.value.productModelName = scanData.model;
      form.value.unit = scanData.unit;
      form.value.qualitity = 1;
      form.value.warnNum = 0;
      form.value.remark = "";
      showForm.value = true;
    } catch (error) {
      console.error("解析二维码失败", error);
      modal.msgError("解析二维码失败,请确保扫码内容正确");
    }
  };
  const handleSubmit = async () => {
    try {
      const valid = await formRef.value.validate();
      if (!valid) return;
      loading.value = true;
      const apiCall =
        type.value === "qualified"
          ? createStockInventory
          : createStockUnInventory;
      const res = await apiCall(form.value);
      if (res.code === 200) {
        modal.msgSuccess("入库成功");
        setTimeout(() => {
          showForm.value = false;
        }, 1500);
      }
    } catch (error) {
      console.error("提交失败", error);
    } finally {
      loading.value = false;
    }
  };
</script>
<style scoped lang="scss">
  .scan-container {
    min-height: 100vh;
    background-color: #f5f7fa;
  }
  .module-selector {
    display: flex;
    flex-direction: column;
    padding: 40rpx;
    height: 80vh;
    justify-content: center;
  }
  .module-card {
    display: flex;
    align-items: center;
    background-color: #fff;
    padding: 80rpx 50rpx;
    border-radius: 32rpx;
    box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.05);
    margin-bottom: 50rpx;
    transition: all 0.3s ease;
    border: 2rpx solid transparent;
    &:active {
      transform: scale(0.98);
      background-color: #f9f9f9;
    }
  }
  .module-icon {
    width: 140rpx;
    height: 140rpx;
    border-radius: 32rpx;
    display: flex;
    justify-content: center;
    align-items: center;
    margin-right: 40rpx;
    &.qualified {
      background: linear-gradient(135deg, #52c41a, #73d13d);
      box-shadow: 0 10rpx 20rpx rgba(82, 196, 26, 0.2);
    }
    &.unqualified {
      background: linear-gradient(135deg, #ff4d4f, #ff7875);
      box-shadow: 0 10rpx 20rpx rgba(255, 77, 79, 0.2);
    }
  }
  .module-info {
    display: flex;
    flex-direction: column;
  }
  .module-label {
    font-size: 40rpx;
    font-weight: 700;
    color: #1a1a1a;
    margin-bottom: 12rpx;
  }
  .module-desc {
    font-size: 28rpx;
    color: #999;
  }
  .form-content {
    background-color: #fff;
    margin: 20rpx;
    padding: 30rpx;
    border-radius: 16rpx;
  }
  .footer-btns {
    margin-top: 60rpx;
    display: flex;
    justify-content: space-between;
    padding-bottom: 40rpx;
  }
  .cancel-btn {
    width: 30%;
    background-color: #f5f5f5;
    color: #666;
    border: none;
  }
  .save-btn {
    width: 65%;
    background: linear-gradient(140deg, #00baff 0%, #006cfb 100%);
    color: #fff;
    border: none;
  }
</style>
src/pages/inventoryManagement/scanOut/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,339 @@
<template>
  <view class="scan-container">
    <PageHeader title="扫码出库"
                @back="goBack" />
    <view class="module-selector"
          v-if="!showForm">
      <view class="module-card"
            @click="startScan('qualified')">
        <view class="module-icon qualified">
          <u-icon name="checkbox-mark"
                  color="#fff"
                  size="40"></u-icon>
        </view>
        <view class="module-info">
          <text class="module-label">合格出库</text>
          <text class="module-desc">扫描合格品进行领用出库</text>
        </view>
      </view>
      <view class="module-card"
            @click="startScan('unqualified')">
        <view class="module-icon unqualified">
          <u-icon name="close"
                  color="#fff"
                  size="40"></u-icon>
        </view>
        <view class="module-info">
          <text class="module-label">不合格出库</text>
          <text class="module-desc">记录不合格品的出库流向</text>
        </view>
      </view>
    </view>
    <view class="form-content"
          v-if="showForm">
      <u-form ref="formRef"
              :model="form"
              :rules="formRules"
              label-width="100px">
        <u-form-item label="出库类型"
                     border-bottom>
          <u-tag :text="type === 'qualified' ? '合格出库' : '不合格出库'"
                 :type="type === 'qualified' ? 'success' : 'error'"></u-tag>
        </u-form-item>
        <u-form-item label="产品名称"
                     border-bottom>
          <u-input v-model="form.productName"
                   readonly
                   border="none"></u-input>
        </u-form-item>
        <u-form-item label="规格型号"
                     border-bottom>
          <u-input v-model="form.model"
                   readonly
                   border="none"></u-input>
        </u-form-item>
        <u-form-item label="可用库存"
                     border-bottom>
          <u-input v-model="form.unLockedQuantity"
                   readonly
                   border="none"></u-input>{{form.unit}}
        </u-form-item>
        <u-form-item label="出库数量"
                     prop="qualitity"
                     required
                     border-bottom>
          <u-number-box v-model="form.qualitity"
                        :min="1"
                        :max="form.unLockedQuantity"
                        :step="1"></u-number-box>
          <text class="limit-tip">最大可领用: {{form.unLockedQuantity}}</text>
        </u-form-item>
        <u-form-item label="备注"
                     prop="remark"
                     border-bottom>
          <u-textarea v-model="form.remark"
                      placeholder="请输入备注"
                      count></u-textarea>
        </u-form-item>
      </u-form>
      <view class="footer-btns">
        <u-button class="cancel-btn"
                  @click="cancelForm">取消</u-button>
        <u-button class="save-btn"
                  @click="handleSubmit"
                  :loading="loading">确认出库</u-button>
      </view>
    </view>
  </view>
</template>
<script setup>
  import { ref, reactive } from "vue";
  import PageHeader from "@/components/PageHeader.vue";
  import {
    subtractStockInventory,
    getStockInventoryListPage,
  } from "@/api/inventoryManagement/stockInventory.js";
  import {
    subtractStockUnInventory,
    getStockUninventoryListPage,
  } from "@/api/inventoryManagement/stockUninventory.js";
  import modal from "@/plugins/modal";
  const showForm = ref(false);
  const type = ref("qualified"); // qualified | unqualified
  const loading = ref(false);
  const formRef = ref(null);
  const form = ref({
    id: undefined,
    productId: undefined,
    productModelId: undefined,
    productName: "",
    model: "",
    unit: "",
    qualitity: 1,
    unLockedQuantity: 0,
    remark: "",
  });
  const formRules = {
    qualitity: [
      {
        required: true,
        type: "number",
        message: "请输入出库数量",
        trigger: ["blur", "change"],
      },
      {
        validator: (rule, value, callback) => {
          if (value > form.value.unLockedQuantity) {
            callback(new Error("不能超过可用库存"));
          } else {
            callback();
          }
        },
        trigger: ["blur", "change"],
      },
    ],
  };
  const goBack = () => {
    if (showForm.value) {
      showForm.value = false;
    } else {
      uni.navigateBack();
    }
  };
  const cancelForm = () => {
    showForm.value = false;
  };
  const startScan = scanType => {
    type.value = scanType;
    uni.scanCode({
      success: res => {
        handleScanResult(res.result);
      },
      fail: err => {
        modal.msgError("扫码失败");
      },
    });
  };
  const handleScanResult = async result => {
    try {
      // è§£æžäºŒç»´ç æ•°æ®
      const scanData = JSON.parse(result);
      if (!scanData.id) {
        modal.msgError("无效的二维码数据");
        return;
      }
      // èŽ·å–å®žæ—¶åº“å­˜è¯¦æƒ…
      modal.loading("获取产品库存详情...");
      const apiCall =
        type.value === "qualified"
          ? getStockInventoryListPage
          : getStockUninventoryListPage;
      const res = await apiCall({ productModelId: scanData.id });
      modal.closeLoading();
      if (res.code === 200 && res.data.records && res.data.records.length > 0) {
        const detail = res.data.records[0];
        form.value.id = detail.id;
        form.value.productId = detail.productId;
        form.value.productName = detail.productName;
        form.value.productModelId = detail.productModelId;
        form.value.model = detail.model;
        form.value.unit = detail.unit;
        form.value.unLockedQuantity = detail.unLockedQuantity;
        form.value.qualitity = 1;
        form.value.remark = "";
        if (form.value.unLockedQuantity <= 0) {
          modal.msgError("当前库存不足,无法出库");
          return;
        }
        showForm.value = true;
      } else {
        modal.msgError("未找到该产品型号的库存记录");
      }
    } catch (error) {
      modal.closeLoading();
      console.error("处理扫码结果失败", error);
      modal.msgError("扫码处理失败,请重试");
    }
  };
  const handleSubmit = async () => {
    try {
      const valid = await formRef.value.validate();
      if (!valid) return;
      loading.value = true;
      const apiCall =
        type.value === "qualified"
          ? subtractStockInventory
          : subtractStockUnInventory;
      const res = await apiCall(form.value);
      if (res.code === 200) {
        modal.msgSuccess("出库成功");
        setTimeout(() => {
          showForm.value = false;
        }, 1500);
      }
    } catch (error) {
      console.error("提交失败", error);
    } finally {
      loading.value = false;
    }
  };
</script>
<style scoped lang="scss">
  .scan-container {
    min-height: 100vh;
    background-color: #f5f7fa;
  }
  .module-selector {
    display: flex;
    flex-direction: column;
    padding: 40rpx;
    height: 80vh;
    justify-content: center;
  }
  .module-card {
    display: flex;
    align-items: center;
    background-color: #fff;
    padding: 80rpx 50rpx;
    border-radius: 32rpx;
    box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.05);
    margin-bottom: 50rpx;
    transition: all 0.3s ease;
    border: 2rpx solid transparent;
    &:active {
      transform: scale(0.98);
      background-color: #f9f9f9;
    }
  }
  .module-icon {
    width: 140rpx;
    height: 140rpx;
    border-radius: 32rpx;
    display: flex;
    justify-content: center;
    align-items: center;
    margin-right: 40rpx;
    &.qualified {
      background: linear-gradient(135deg, #52c41a, #73d13d);
      box-shadow: 0 10rpx 20rpx rgba(82, 196, 26, 0.2);
    }
    &.unqualified {
      background: linear-gradient(135deg, #ff4d4f, #ff7875);
      box-shadow: 0 10rpx 20rpx rgba(255, 77, 79, 0.2);
    }
  }
  .module-info {
    display: flex;
    flex-direction: column;
  }
  .module-label {
    font-size: 40rpx;
    font-weight: 700;
    color: #1a1a1a;
    margin-bottom: 12rpx;
  }
  .module-desc {
    font-size: 28rpx;
    color: #999;
  }
  .form-content {
    background-color: #fff;
    margin: 20rpx;
    padding: 30rpx;
    border-radius: 16rpx;
  }
  .limit-tip {
    font-size: 24rpx;
    color: #999;
    margin-left: 20rpx;
  }
  .footer-btns {
    margin-top: 60rpx;
    display: flex;
    justify-content: space-between;
    padding-bottom: 40rpx;
  }
  .cancel-btn {
    width: 30%;
    background-color: #f5f5f5;
    color: #666;
    border: none;
  }
  .save-btn {
    width: 65%;
    background: linear-gradient(140deg, #00baff 0%, #006cfb 100%);
    color: #fff;
    border: none;
  }
</style>
src/pages/works.vue
@@ -15,7 +15,8 @@
                        :key="index"
                        @click="handleCommonItemClick(item)">
            <view class="icon-container">
              <image :src="item.icon" class="item-icon"></image>
              <image :src="item.icon"
                     class="item-icon"></image>
            </view>
            <text class="item-label">{{item.label}}</text>
          </up-grid-item>
@@ -37,7 +38,8 @@
                        :key="index"
                        @click="handleCommonItemClick(item)">
            <view class="icon-container">
              <image :src="item.icon" class="item-icon"></image>
              <image :src="item.icon"
                     class="item-icon"></image>
            </view>
            <text class="item-label">{{item.label}}</text>
          </up-grid-item>
@@ -59,7 +61,8 @@
                        :key="index"
                        @click="handleCommonItemClick(item)">
            <view class="icon-container">
              <image :src="item.icon" class="item-icon"></image>
              <image :src="item.icon"
                     class="item-icon"></image>
            </view>
            <text class="item-label">{{item.label}}</text>
          </up-grid-item>
@@ -81,9 +84,24 @@
                        :key="index"
                        @click="handleCommonItemClick(item)">
            <view class="icon-container">
              <image :src="item.icon" class="item-icon"></image>
              <image :src="item.icon"
                     class="item-icon"></image>
            </view>
            <text class="item-label">{{item.label}}</text>
          </up-grid-item>
          <up-grid-item @click="jumpUrl('/pages/inventoryManagement/scanIn/index')">
            <view class="icon-container">
              <image src="/static/images/icon/xiaoshoutaizhang.svg"
                     class="item-icon"></image>
            </view>
            <text class="item-label">扫码入库</text>
          </up-grid-item>
          <up-grid-item @click="jumpUrl('/pages/inventoryManagement/scanOut/index')">
            <view class="icon-container">
              <image src="/static/images/icon/xiaoshoutaizhang.svg"
                     class="item-icon"></image>
            </view>
            <text class="item-label">扫码出库</text>
          </up-grid-item>
        </up-grid>
      </view>
@@ -149,7 +167,8 @@
                        :key="index"
                        @click="handleCommonItemClick(item)">
            <view class="icon-container">
              <image :src="item.icon" class="item-icon"></image>
              <image :src="item.icon"
                     class="item-icon"></image>
            </view>
            <text class="item-label">{{item.label}}</text>
          </up-grid-item>
@@ -171,7 +190,8 @@
                        :key="index"
                        @click="handleCommonItemClick(item)">
            <view class="icon-container">
              <image :src="item.icon" class="item-icon"></image>
              <image :src="item.icon"
                     class="item-icon"></image>
            </view>
            <text class="item-label">{{item.label}}</text>
          </up-grid-item>
@@ -193,7 +213,8 @@
                        :key="index"
                        @click="handleCommonItemClick(item)">
            <view class="icon-container">
              <image :src="item.icon" class="item-icon"></image>
              <image :src="item.icon"
                     class="item-icon"></image>
            </view>
            <text class="item-label">{{item.label}}</text>
          </up-grid-item>
@@ -215,7 +236,8 @@
                        :key="index"
                        @click="handleCommonItemClick(item)">
            <view class="icon-container">
              <image :src="item.icon" class="item-icon"></image>
              <image :src="item.icon"
                     class="item-icon"></image>
            </view>
            <text class="item-label">{{item.label}}</text>
          </up-grid-item>
@@ -237,7 +259,8 @@
                        :key="index"
                        @click="handleCommonItemClick(item)">
            <view class="icon-container">
              <image :src="item.icon" class="item-icon"></image>
              <image :src="item.icon"
                     class="item-icon"></image>
            </view>
            <text class="item-label">{{item.label}}</text>
          </up-grid-item>
@@ -259,7 +282,8 @@
                        :key="index"
                        @click="handleCommonItemClick(item)">
            <view class="icon-container">
              <image :src="item.icon" class="item-icon"></image>
              <image :src="item.icon"
                     class="item-icon"></image>
            </view>
            <text class="item-label">{{item.label}}</text>
          </up-grid-item>
@@ -532,8 +556,8 @@
  // è®¾å¤‡ç®¡ç†åŠŸèƒ½æ•°æ®
  const equipmentItems = reactive([
    {
        icon: '/static/images/icon/shengchanbaogong.svg',
        label: '设备台账',
      icon: "/static/images/icon/shengchanbaogong.svg",
      label: "设备台账",
    },
    {
      icon: "/static/images/icon/yunxingguanli.svg",
@@ -912,6 +936,11 @@
        });
    }
  };
  const jumpUrl = url => {
    uni.navigateTo({
      url: url,
    });
  };
  // åˆ›å»ºå¯¹å­ç»„件的引用
  const uToastRef = ref(null);
@@ -1092,10 +1121,16 @@
    // å®šä¹‰èœå•配置映射
    const menuMapping = {
      collaboration: { target: collaborationItems, specialMapping: { "规章制度": "规章制度管理" } },
      archiveManagement: { target: archiveManagementItems, specialMapping: { "供应商档案": "供应商管理" } },
      collaboration: {
        target: collaborationItems,
        specialMapping: { è§„章制度: "规章制度管理" },
      },
      archiveManagement: {
        target: archiveManagementItems,
        specialMapping: { ä¾›åº”商档案: "供应商管理" },
      },
    };
    console.log(allowedMenuTitles)
    console.log(allowedMenuTitles);
    // é€šç”¨è¿‡æ»¤å‡½æ•°
    const filterArray = (targetArray, specialMapping) => {
      const filtered = targetArray.filter(item => {
@@ -1112,7 +1147,10 @@
    filterArray(marketingItems);
    filterArray(purchaseItems);
    filterArray(financeManagementItems);
    filterArray(archiveManagementItems, menuMapping.archiveManagement.specialMapping);
    filterArray(
      archiveManagementItems,
      menuMapping.archiveManagement.specialMapping
    );
    filterArray(collaborationItems, menuMapping.collaboration.specialMapping);
    filterArray(safetyItems);
    filterArray(humanResourcesItems);
@@ -1125,14 +1163,22 @@
  // æ£€æŸ¥æ¨¡å—是否有菜单项需要显示
  const hasMarketingItems = computed(() => marketingItems.length > 0);
  const hasPurchaseItems = computed(() => purchaseItems.length > 0);
  const hasFinanceManagementItems = computed(() => financeManagementItems.length > 0);
  const hasArchiveManagementItems = computed(() => archiveManagementItems.length > 0);
  const hasAfterSalesServiceItems = computed(() => afterSalesServiceItems.length > 0);
  const hasFinanceManagementItems = computed(
    () => financeManagementItems.length > 0
  );
  const hasArchiveManagementItems = computed(
    () => archiveManagementItems.length > 0
  );
  const hasAfterSalesServiceItems = computed(
    () => afterSalesServiceItems.length > 0
  );
  const hasCollaborationItems = computed(() => collaborationItems.length > 0);
  const hasSafetyItems = computed(() => safetyItems.length > 0);
  const hasQualityItems = computed(() => qualityItems.length > 0);
  // const hasHumanResourcesItems = computed(() => humanResourcesItems.length > 0);
  const hasWarehouseLogisticsItems = computed(() => warehouseLogisticsItems.length > 0);
  const hasWarehouseLogisticsItems = computed(
    () => warehouseLogisticsItems.length > 0
  );
  // const hasProductionItems = computed(() => productionItems.length > 0);
  const hasEquipmentItems = computed(() => equipmentItems.length > 0);