gaoluyang
2026-06-01 ab8ff90598b99b1b88b925c03e82e20ff0426fcf
新疆马铃薯
1.库存管理绑定设备,添加查看数采功能
已添加2个文件
已修改5个文件
609 ■■■■■ 文件已修改
src/api/inventoryManagement/stockInventory.js 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/notificationManagement/meetApplication/index.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/BindDeviceDialog.vue 145 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/IotDataDialog.vue 330 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/New.vue 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/Record.vue 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/Subtract.vue 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inventoryManagement/stockInventory.js
@@ -103,3 +103,20 @@
    });
};
// èŽ·å–ç‰©è”è®¾å¤‡å®žæ—¶æ•°é‡‡æ•°æ®
export const getIotRealtimeData = (id) => {
    return request({
        url: `/stockInventory/iotRealtime/${id}`,
        method: "get",
    });
};
// ç»‘定物联设备到库存
export const bindIotDevice = (id, warehouse) => {
    return request({
        url: `/stockInventory/bindIotDevice/${id}`,
        method: "put",
        data: { warehouse },
    });
};
src/views/collaborativeApproval/notificationManagement/meetApplication/index.vue
@@ -126,9 +126,9 @@
          >
            <el-option
                v-for="person in employees"
                :key="person.id"
                :label="`${person.staffName}${person.postName ? ` (${person.postName})` : ''}`"
                :value="person.id"
                :key="person.userId"
                :label="`${person.nickName || person.userName}${person.dept?.deptName ? ` (${person.dept.deptName})` : ''}`"
                :value="person.userId"
            />
          </el-select>
        </el-form-item>
@@ -156,7 +156,7 @@
import {ElMessage} from 'element-plus'
import {Plus, Document, Promotion, Bell} from '@element-plus/icons-vue'
import {getRoomEnum, saveMeetingApplication} from '@/api/collaborativeApproval/meeting.js'
import {staffOnJobListPage} from "@/api/personnelManagement/staffOnJob.js";
import {userListNoPageByTenantId} from "@/api/system/user.js";
// å½“前申请类型
const currentType = ref('department') // approval: å®¡æ‰¹æµç¨‹, department: éƒ¨é—¨çº§, notification: é€šçŸ¥å‘布
@@ -415,12 +415,8 @@
  getRoomEnum().then(res => {
    meetingRooms.value = res.data
  })
  staffOnJobListPage({
    current: -1,
    size: -1,
    staffState: 1
  }).then(res => {
    employees.value = res.data.records.sort((a, b) => (a.postName || '').localeCompare(b.postName || ''))
  userListNoPageByTenantId().then(res => {
    employees.value = res.data || []
  })
})
</script>
src/views/inventoryManagement/stockManagement/BindDeviceDialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,145 @@
<template>
  <div>
    <el-dialog v-model="isShow"
               title="绑定物联设备"
               width="600"
               @close="closeModal">
      <el-form label-width="100px"
               :model="formState"
               ref="formRef">
        <el-form-item label="产品名称">
          <el-input v-model="props.record.productName" disabled />
        </el-form-item>
        <el-form-item label="规格型号">
          <el-input v-model="props.record.model" disabled />
        </el-form-item>
        <el-form-item label="批号">
          <el-input v-model="props.record.batchNo" disabled />
        </el-form-item>
        <el-form-item label="物联设备"
                      prop="deviceIds"
                      :rules="[
                        {
                          required: false,
                          message: '请选择物联设备',
                          trigger: 'change',
                        }
                      ]">
          <el-select v-model="formState.deviceIds"
                     multiple
                     filterable
                     placeholder="请选择物联设备"
                     style="width: 100%">
            <el-option v-for="item in deviceOptions"
                       :key="item.id"
                       :label="`${item.deviceName} (${item.deviceModel})`"
                       :value="item.id">
              <span style="float: left">{{ item.deviceName }}</span>
              <span style="float: right; color: #8492a6; font-size: 13px">{{ item.deviceModel }}</span>
            </el-option>
          </el-select>
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="handleSubmit" :loading="submitLoading">确认</el-button>
          <el-button @click="closeModal">取消</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
  import { ref, computed, onMounted, getCurrentInstance } from "vue";
  import { getLedgerPage } from "@/api/equipmentManagement/ledger.js";
  import { bindIotDevice } from "@/api/inventoryManagement/stockInventory.js";
  const props = defineProps({
    visible: {
      type: Boolean,
      required: true,
    },
    record: {
      type: Object,
      default: () => ({}),
    },
  });
  const emit = defineEmits(["update:visible", "completed"]);
  const { proxy } = getCurrentInstance();
  const isShow = computed({
    get() {
      return props.visible;
    },
    set(val) {
      emit("update:visible", val);
    },
  });
  const formRef = ref(null);
  const submitLoading = ref(false);
  const deviceOptions = ref([]);
  const formState = ref({
    deviceIds: [],
  });
  // èŽ·å–ç‰©è”è®¾å¤‡åˆ—è¡¨
  const getDeviceOptions = async () => {
    try {
      const res = await getLedgerPage({
        isIotDevice: 1,
        page: 1,
        size: 999,
      });
      if (res.data && res.data.records) {
        deviceOptions.value = res.data.records;
      }
    } catch (error) {
      console.error("获取物联设备列表失败:", error);
      proxy.$modal.msgError("获取物联设备列表失败");
    }
  };
  // åˆå§‹åŒ–已绑定设备
  const initSelectedDevices = () => {
    if (props.record.warehouse) {
      // warehouse å­—段存储的是逗号分隔的设备ID
      const deviceIds = props.record.warehouse.split(",").map(id => Number(id.trim())).filter(id => !isNaN(id));
      formState.value.deviceIds = deviceIds;
    }
  };
  const closeModal = () => {
    formState.value.deviceIds = [];
    isShow.value = false;
  };
  const handleSubmit = () => {
    formRef.value.validate(valid => {
      if (valid) {
        submitLoading.value = true;
        // å°†è®¾å¤‡ID数组转换为逗号分隔的字符串
        const warehouse = formState.value.deviceIds.join(",");
        bindIotDevice(props.record.id, warehouse)
          .then(res => {
            submitLoading.value = false;
            proxy.$modal.msgSuccess("绑定成功");
            closeModal();
            emit("completed");
          })
          .catch(() => {
            submitLoading.value = false;
          });
      }
    });
  };
  onMounted(() => {
    getDeviceOptions();
    initSelectedDevices();
  });
</script>
src/views/inventoryManagement/stockManagement/IotDataDialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,330 @@
<template>
  <div>
    <el-dialog v-model="isShow"
               title="物联设备实时数采"
               width="800"
               @close="closeModal">
      <div v-if="loading" v-loading="loading" element-loading-text="加载中..." style="min-height: 200px;"></div>
      <div v-else-if="!hasDevices" class="empty-state">
        <el-empty description="暂无绑定的物联设备">
          <el-button type="primary" @click="closeModal">去绑定</el-button>
        </el-empty>
      </div>
      <div v-else>
        <div class="device-header">
          <div class="refresh-switch">
            <el-switch v-model="autoRefresh" active-text="自动刷新(30s)" />
          </div>
          <el-button type="primary" size="small" @click="fetchData" :loading="loading">
            <el-icon><Refresh /></el-icon>刷新
          </el-button>
        </div>
        <div class="devices-container">
          <el-card v-for="device in deviceData.devices" :key="device.deviceId" class="device-card">
            <template #header>
              <div class="device-header-info">
                <div class="device-title">
                  <span class="device-name">{{ device.deviceName }}</span>
                  <el-tag size="small" type="info">{{ device.deviceModel }}</el-tag>
                </div>
                <div class="device-status">
                  <span class="status-dot" :class="getStatusClass(device.status)"></span>
                  <span :class="getStatusTextClass(device.status)">{{ device.status || '未知' }}</span>
                </div>
              </div>
            </template>
            <div v-if="device.status === '在线'" class="device-data">
              <el-row :gutter="10">
                <el-col :span="8" v-if="device.temperature">
                  <div class="data-item">
                    <el-icon class="data-icon"><Sunny /></el-icon>
                    <div class="data-info">
                      <div class="data-label">温度</div>
                      <div class="data-value">{{ device.temperature }}</div>
                    </div>
                  </div>
                </el-col>
                <el-col :span="8" v-if="device.humidity">
                  <div class="data-item">
                    <el-icon class="data-icon"><Drizzling /></el-icon>
                    <div class="data-info">
                      <div class="data-label">湿度</div>
                      <div class="data-value">{{ device.humidity }}</div>
                    </div>
                  </div>
                </el-col>
                <el-col :span="8" v-if="device.co2">
                  <div class="data-item">
                    <el-icon class="data-icon"><WindPower /></el-icon>
                    <div class="data-info">
                      <div class="data-label">CO2</div>
                      <div class="data-value">{{ device.co2 }}</div>
                    </div>
                  </div>
                </el-col>
                <el-col :span="8" v-if="device.light">
                  <div class="data-item">
                    <el-icon class="data-icon"><Sunrise /></el-icon>
                    <div class="data-info">
                      <div class="data-label">光照</div>
                      <div class="data-value">{{ device.light }}</div>
                    </div>
                  </div>
                </el-col>
                <el-col :span="8" v-if="device.battery">
                  <div class="data-item">
                    <el-icon class="data-icon"><Lightning /></el-icon>
                    <div class="data-info">
                      <div class="data-label">电量</div>
                      <div class="data-value">{{ device.battery }}</div>
                    </div>
                  </div>
                </el-col>
              </el-row>
            </div>
            <div v-else class="device-offline">
              <el-alert :title="device.statusMessage || '设备离线'" type="warning" :closable="false" show-icon />
            </div>
          </el-card>
        </div>
      </div>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="closeModal">关闭</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
  import { ref, computed, onMounted, onUnmounted, watch } from "vue";
  import { getIotRealtimeData } from "@/api/inventoryManagement/stockInventory.js";
  import { Refresh, Sunny, Drizzling, WindPower, Sunrise, Lightning } from "@element-plus/icons-vue";
  const props = defineProps({
    visible: {
      type: Boolean,
      required: true,
    },
    record: {
      type: Object,
      default: () => ({}),
    },
  });
  const emit = defineEmits(["update:visible"]);
  const isShow = computed({
    get() {
      return props.visible;
    },
    set(val) {
      emit("update:visible", val);
    },
  });
  const loading = ref(false);
  const deviceData = ref({
    inventoryId: null,
    iotDeviceIds: "",
    devices: [],
  });
  const autoRefresh = ref(false);
  let refreshTimer = null;
  const hasDevices = computed(() => {
    return deviceData.value.devices && deviceData.value.devices.length > 0;
  });
  const getStatusClass = (status) => {
    switch (status) {
      case "在线":
        return "status-online";
      case "offline":
        return "status-offline";
      case "error":
        return "status-error";
      default:
        return "status-offline";
    }
  };
  const getStatusTextClass = (status) => {
    switch (status) {
      case "在线":
        return "text-online";
      case "offline":
        return "text-offline";
      case "error":
        return "text-error";
      default:
        return "text-offline";
    }
  };
  const fetchData = async () => {
    if (!props.record.id) return;
    loading.value = true;
    try {
      const res = await getIotRealtimeData(props.record.id);
      if (res.code === 200 && res.data) {
        deviceData.value = res.data;
      }
    } catch (error) {
      console.error("获取物联设备数据失败:", error);
    } finally {
      loading.value = false;
    }
  };
  const closeModal = () => {
    autoRefresh.value = false;
    isShow.value = false;
  };
  // è‡ªåŠ¨åˆ·æ–°
  watch(autoRefresh, (val) => {
    if (val) {
      refreshTimer = setInterval(() => {
        fetchData();
      }, 30000);
    } else {
      if (refreshTimer) {
        clearInterval(refreshTimer);
        refreshTimer = null;
      }
    }
  });
  onMounted(() => {
    fetchData();
  });
  onUnmounted(() => {
    if (refreshTimer) {
      clearInterval(refreshTimer);
    }
  });
</script>
<style scoped>
  .empty-state {
    padding: 40px 0;
  }
  .device-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 20px;
  }
  .refresh-switch {
    display: flex;
    align-items: center;
  }
  .devices-container {
    display: flex;
    flex-direction: column;
    gap: 15px;
  }
  .device-card {
    margin-bottom: 10px;
  }
  .device-header-info {
    display: flex;
    justify-content: space-between;
    align-items: center;
  }
  .device-title {
    display: flex;
    align-items: center;
    gap: 10px;
  }
  .device-name {
    font-weight: bold;
    font-size: 16px;
  }
  .device-status {
    display: flex;
    align-items: center;
    gap: 5px;
  }
  .status-dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    display: inline-block;
  }
  .status-online {
    background-color: #67c23a;
  }
  .status-offline {
    background-color: #909399;
  }
  .status-error {
    background-color: #f56c6c;
  }
  .text-online {
    color: #67c23a;
  }
  .text-offline {
    color: #909399;
  }
  .text-error {
    color: #f56c6c;
  }
  .device-data {
    padding: 10px 0;
  }
  .data-item {
    display: flex;
    align-items: center;
    padding: 10px;
    background-color: #f5f7fa;
    border-radius: 4px;
    margin-bottom: 10px;
  }
  .data-icon {
    font-size: 24px;
    color: #409eff;
    margin-right: 10px;
  }
  .data-info {
    flex: 1;
  }
  .data-label {
    font-size: 12px;
    color: #909399;
    margin-bottom: 2px;
  }
  .data-value {
    font-size: 16px;
    font-weight: bold;
    color: #303133;
  }
  .device-offline {
    padding: 20px 0;
  }
</style>
src/views/inventoryManagement/stockManagement/New.vue
@@ -49,23 +49,6 @@
                       value="unqualified" />
          </el-select>
        </el-form-item>
        <el-form-item label="仓库"
                      prop="warehouse"
                      :rules="[
                {
                required: true,
                message: '请选择仓库',
                trigger: 'change',
              }
            ]">
          <el-select v-model="formState.warehouse"
                     placeholder="请选择仓库">
            <el-option v-for="item in warehouseOptions"
                       :key="item.value"
                       :label="item.label"
                       :value="item.value" />
          </el-select>
        </el-form-item>
        <el-form-item label="库存数量"
                      prop="qualitity">
          <el-input-number v-model="formState.qualitity"
@@ -119,11 +102,10 @@
</template>
<script setup>
  import { ref, computed, watch, getCurrentInstance, onMounted } from "vue";
  import { ref, computed, watch, getCurrentInstance } from "vue";
  import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
  import { addStockInRecordOnly } from "@/api/inventoryManagement/stockInventory.js";
  import { createStockUnInventory } from "@/api/inventoryManagement/stockUninventory.js";
  import { getDicts } from "@/api/system/dict/data";
  const props = defineProps({
    visible: {
@@ -146,7 +128,6 @@
    productName: "",
    productModelName: "",
    unit: "",
    warehouse: undefined,
    type: undefined,
    qualitity: 0,
    batchNo: null,
@@ -154,9 +135,6 @@
    createTime: "",
    remark: "",
  });
  // ä»“库选项
  const warehouseOptions = ref([]);
  const isShow = computed({
    get() {
@@ -168,21 +146,6 @@
  });
  const showProductSelectDialog = ref(false);
  // èŽ·å–ä»“åº“å­—å…¸æ•°æ®
  const getWarehouseOptions = async () => {
    const res = await getDicts("warehouse");
    if (res.code === 200) {
      warehouseOptions.value = res.data.map(item => ({
        label: item.dictLabel,
        value: item.dictValue,
      }));
    }
  };
  onMounted(() => {
    getWarehouseOptions();
  });
  // æ‰¹å·ä¸ºç©ºæ—¶è½¬ä¸º null
  watch(
@@ -204,7 +167,6 @@
      productName: "",
      productModelName: "",
      unit: "",
      warehouse: undefined,
      type: undefined,
      qualitity: 0,
      batchNo: null,
src/views/inventoryManagement/stockManagement/Record.vue
@@ -113,12 +113,18 @@
                         show-overflow-tooltip />
        <el-table-column fixed="right"
                         label="操作"
                         min-width="80"
                         min-width="190"
                         align="center">
          <template #default="scope">
            <el-button link
                       type="primary"
                       @click="showDetailModal(scope.row)">详情</el-button>
            <el-button link
                       type="success"
                       @click="showBindDeviceModal(scope.row)">绑定设备</el-button>
            <el-button link
                       type="info"
                       @click="showIotDataModal(scope.row)">查看数采</el-button>
          </template>
        </el-table-column>
      </el-table>
@@ -156,6 +162,15 @@
                                     :operation-type="operationType"
                                     :type="record.stockType"
                                     @completed="handleQuery" />
    <!-- ç»‘定物联设备 -->
    <bind-device-dialog v-if="isShowBindDeviceModal"
                        v-model:visible="isShowBindDeviceModal"
                        :record="record"
                        @completed="handleQuery" />
    <!-- æŸ¥çœ‹ç‰©è”设备数采 -->
    <iot-data-dialog v-if="isShowIotDataModal"
                     v-model:visible="isShowIotDataModal"
                     :record="record" />
  </div>
</template>
@@ -187,6 +202,12 @@
  const BatchNoQtyDetail = defineAsyncComponent(() =>
    import("@/views/inventoryManagement/stockManagement/BatchNoQtyDetail.vue")
  );
  const BindDeviceDialog = defineAsyncComponent(() =>
    import("@/views/inventoryManagement/stockManagement/BindDeviceDialog.vue")
  );
  const IotDataDialog = defineAsyncComponent(() =>
    import("@/views/inventoryManagement/stockManagement/IotDataDialog.vue")
  );
  const { proxy } = getCurrentInstance();
  const tableData = ref([]);
  const selectedRows = ref([]);
@@ -209,6 +230,10 @@
  const operationType = ref("frozen");
  // æ˜¯å¦æ˜¾ç¤ºå¯¼å…¥å¼¹æ¡†
  const isShowImportModal = ref(false);
  // æ˜¯å¦æ˜¾ç¤ºç»‘定设备弹框
  const isShowBindDeviceModal = ref(false);
  // æ˜¯å¦æ˜¾ç¤ºç‰©è”数采弹框
  const isShowIotDataModal = ref(false);
  const data = reactive({
    searchForm: {
      productName: "",
@@ -308,6 +333,18 @@
    operationType.value = "thaw";
  };
  // ç‚¹å‡»ç»‘定设备
  const showBindDeviceModal = row => {
    record.value = row;
    isShowBindDeviceModal.value = true;
  };
  // ç‚¹å‡»æŸ¥çœ‹æ•°é‡‡
  const showIotDataModal = row => {
    record.value = row;
    isShowIotDataModal.value = true;
  };
  // è¡¨æ ¼é€‰æ‹©æ•°æ®
  const handleSelectionChange = selection => {
    // è¿‡æ»¤æŽ‰å­æ•°æ®
src/views/inventoryManagement/stockManagement/Subtract.vue
@@ -61,6 +61,16 @@
          <el-input-number v-model="formState.qualitity" :step="1" :min="1" :max="maxQuality" style="width: 100%" />
        </el-form-item>
        <el-form-item label="出库批次" prop="outboundBatches">
          <el-input
            v-model="formState.outboundBatches"
            placeholder="留空自动生成"
            maxlength="100"
            show-word-limit
          />
          <div class="form-tip">不填则自动生成,格式:CK年月日-序号</div>
        </el-form-item>
        <el-form-item label="备注" prop="remark">
          <el-input v-model="formState.remark" type="textarea" />
        </el-form-item>
@@ -134,6 +144,7 @@
  model: "",
  unit: "",
  qualitity: 0,
  outboundBatches: "",
  remark: '',
});
@@ -157,6 +168,7 @@
    productModelId: undefined,
    productName: "",
    productModelName: "",
    outboundBatches: "",
    description: '',
  };
  isShow.value = false;
@@ -216,4 +228,12 @@
  handleSubmit,
  isShow,
});
</script>
</script>
<style scoped>
.form-tip {
  font-size: 12px;
  color: #909399;
  margin-top: 4px;
}
</style>