gaoluyang
2026-08-28 3f85e5410dda49e7e6a6292859649513f3644cb2
天津豹鸣app
1.添加扫码查看设备变更记录功能
已添加4个文件
已修改4个文件
403 ■■■■■ 文件已修改
src/api/device/statusChangeRecord.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/equipmentManagement/deviceInfo.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/config.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages.json 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/equipmentManagement/deviceInfo/index.vue 275 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/index.vue 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/works.vue 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/static/images/icon/saomashebei.svg 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/device/statusChangeRecord.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,9 @@
import request from '@/utils/request'
export function listStatusChangeRecord(query) {
  return request({
    url: '/device/statusChangeRecord/list',
    method: 'get',
    params: query
  })
}
src/api/equipmentManagement/deviceInfo.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,9 @@
import request from "@/utils/request";
// èŽ·å–è®¾å¤‡åŸºæœ¬ä¿¡æ¯
export function getDeviceInfo(id) {
  return request({
    url: `/device/ledger/${id}`,
    method: "get",
  });
}
src/config.js
@@ -1,6 +1,6 @@
// åº”用全局配置
const config = {
  baseUrl: "http://1.15.17.182:9048",
  baseUrl: "http://192.168.0.31:25615",
  fileUrl: "http://1.15.17.182:9049",
  // åº”用信息
  appInfo: {
src/pages.json
@@ -661,6 +661,13 @@
      }
    },
    {
      "path": "pages/equipmentManagement/deviceInfo/index",
      "style": {
        "navigationBarTitleText": "设备信息",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/equipmentManagement/runManagement/index",
      "style": {
        "navigationBarTitleText": "运行管理",
src/pages/equipmentManagement/deviceInfo/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,275 @@
<template>
  <view class="device-info-container">
    <PageHeader title="设备信息" @back="goBack" />
    <!-- åŸºæœ¬ä¿¡æ¯ -->
    <view class="info-card">
      <view class="card-header">
        <text class="card-title">基本信息</text>
        <text class="device-status">正常</text>
      </view>
      <view class="card-content">
        <view class="info-row">
          <text class="label">设备名称:</text>
          <text class="value">{{ deviceInfo.deviceName || '--' }}</text>
        </view>
        <view class="info-row">
          <text class="label">规格型号:</text>
          <text class="value">{{ deviceInfo.deviceModel || '--' }}</text>
        </view>
        <view class="info-row">
          <text class="label">生产厂家:</text>
          <text class="value">{{ deviceInfo.supplierName || '--' }}</text>
        </view>
        <view class="info-row">
          <text class="label">单位:</text>
          <text class="value">{{ deviceInfo.unit || '--' }}</text>
        </view>
      </view>
    </view>
    <!-- å˜æ›´è®°å½• -->
    <view class="info-card">
      <view class="card-header">
        <text class="card-title">变更记录</text>
      </view>
      <view class="card-content">
        <view v-if="!recordsLoading && changeRecords.length === 0" class="empty-tip">暂无变更记录</view>
        <view v-else class="change-record" v-for="(item, index) in changeRecords" :key="index">
          <view class="record-header">
            <text class="record-time">{{ item.createTime }}</text>
            <text class="record-status" :class="approvalClass(item.approvalStatus)">{{ approvalText(item.approvalStatus) }}</text>
          </view>
          <view class="record-line">
            <text class="record-status-change">{{ statusText(item.originalStatus) }} â†’ {{ statusText(item.targetStatus) }}</text>
            <text class="record-user">申请人:{{ item.createUserName || '--' }}</text>
          </view>
          <view class="record-reason">变更原因:{{ item.reason || '--' }}</view>
          <view v-if="item.approvalStatus === 'REJECTED' && item.rejectReason" class="record-reject">驳回原因:{{ item.rejectReason }}</view>
        </view>
      </view>
    </view>
  </view>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import PageHeader from '@/components/PageHeader.vue'
import { getDeviceInfo } from '@/api/equipmentManagement/deviceInfo'
import { listStatusChangeRecord } from '@/api/device/statusChangeRecord'
const deviceInfo = reactive({
  deviceName: '',
  deviceModel: '',
  supplierName: '',
  unit: '',
})
const changeRecords = ref([])
const recordsLoading = ref(false)
const statusText = (s) => (s == 1 ? '启用' : '停用')
const approvalText = (s) => {
  const map = { PENDING: '审批中', APPROVED: '已通过', REJECTED: '已驳回' }
  return map[s] || s || '--'
}
const approvalClass = (s) => {
  const map = { PENDING: 'status-pending', APPROVED: 'status-approved', REJECTED: 'status-rejected' }
  return map[s] || ''
}
const fetchDeviceInfo = async (deviceId) => {
  try {
    const res = await getDeviceInfo(deviceId)
    console.log('设备信息响应:', res)
    if (res.code === 200 && res.data) {
      Object.assign(deviceInfo, res.data)
    }
  } catch (error) {
    console.error('获取设备信息失败:', error)
  }
}
const fetchChangeRecords = async (deviceId) => {
  recordsLoading.value = true
  try {
    const res = await listStatusChangeRecord({ deviceId, pageNum: 1, pageSize: 20 })
    console.log('变更记录响应:', res)
    if (res.code === 200) {
      changeRecords.value = res.data?.records || res.data || []
    }
  } catch (error) {
    console.error('获取变更记录失败:', error)
  } finally {
    recordsLoading.value = false
  }
}
const goBack = () => {
  uni.navigateBack()
}
onLoad((options) => {
  const deviceId = options?.deviceId || ''
  if (!deviceId) {
    uni.showToast({ title: '缺少设备ID', icon: 'none' })
    return
  }
  fetchDeviceInfo(deviceId)
  fetchChangeRecords(deviceId)
})
</script>
<style scoped lang="scss">
.device-info-container {
  min-height: 100vh;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  padding: 16px 20px 20px;
  box-sizing: border-box;
}
.info-card {
  background: rgba(255, 255, 255, 0.95);
  border-radius: 16px;
  margin-bottom: 16px;
  overflow: hidden;
}
.card-header {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: #ffffff;
  padding: 14px 20px;
  font-weight: 500;
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.card-title {
  font-size: 15px;
  color: #ffffff;
}
.device-status {
  padding: 4px 14px;
  border-radius: 20px;
  font-size: 12px;
  color: #ffffff;
  background: #52c41a;
}
.card-content {
  padding: 16px 20px;
}
.info-row {
  display: flex;
  align-items: flex-start;
  margin-bottom: 12px;
  &:last-child {
    margin-bottom: 0;
  }
}
.label {
  width: 88px;
  flex-shrink: 0;
  color: #666666;
  font-size: 14px;
}
.value {
  flex: 1;
  color: #2c3e50;
  font-weight: 500;
  font-size: 14px;
  word-break: break-all;
}
.status-normal {
  color: #52c41a;
}
.empty-tip {
  color: #999999;
  text-align: center;
  padding: 20px 0;
}
.change-record {
  padding: 14px 0;
  border-bottom: 1px solid #f0f0f0;
  &:last-child {
    border-bottom: none;
  }
}
.record-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 8px;
}
.record-time {
  color: #666666;
  font-size: 13px;
}
.record-status {
  font-size: 12px;
  padding: 2px 10px;
  border-radius: 12px;
  background: #f5f5f5;
  color: #666666;
}
.status-pending {
  background: #fff7e6;
  color: #fa8c16;
}
.status-approved {
  background: #f6ffed;
  color: #52c41a;
}
.status-rejected {
  background: #fff1f0;
  color: #f5222d;
}
.record-line {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 6px;
}
.record-status-change {
  color: #2c3e50;
  font-weight: 500;
  font-size: 14px;
}
.record-user {
  color: #999999;
  font-size: 13px;
}
.record-reason {
  color: #666666;
  font-size: 13px;
}
.record-reject {
  color: #f5222d;
  font-size: 13px;
  margin-top: 4px;
}
</style>
src/pages/index.vue
@@ -231,6 +231,11 @@
    action: "scan",
  },
  {
    label: "扫码查设备",
    icon: "/static/images/icon/saomashebei.svg",
    action: "scanDevice",
  },
  {
    label: "设备巡检",
    icon: "/static/images/icon/xunjianshangchuan.svg",
    route: "/pages/inspectionUpload/index",
@@ -265,6 +270,41 @@
  overviewExpanded.value = !overviewExpanded.value;
}
function getDeviceIdFromScan(scanResult) {
  if (!scanResult) return null;
  const match = scanResult.match(/deviceId=(\d+)/);
  if (match && match[1]) return match[1];
  if (/^\d+$/.test(scanResult.trim())) return scanResult.trim();
  try {
    const obj = JSON.parse(scanResult);
    if (obj.deviceId) return String(obj.deviceId);
  } catch (e) {
    // å¿½ç•¥éž JSON å†…容
  }
  return null;
}
function scanDevice() {
  uni.scanCode({
    scanType: ["qrCode", "barCode"],
    success: (res) => {
      console.log("扫码结果:", res.result);
      const deviceId = getDeviceIdFromScan(res.result);
      if (!deviceId) {
        uni.showToast({ title: "未识别到设备ID", icon: "none" });
        return;
      }
      uni.navigateTo({
        url: `/pages/equipmentManagement/deviceInfo/index?deviceId=${deviceId}`,
      });
    },
    fail: (err) => {
      console.error("扫码失败:", err);
      uni.showToast({ title: "扫码失败,请重试", icon: "none" });
    },
  });
}
function handleQuickTool(item) {
  if (item?.action === "scan") {
    // ç”Ÿäº§æŠ¥å·¥ - è°ƒç”¨æ‰«ç 
@@ -286,6 +326,10 @@
        console.error("扫码失败:", err);
      }
    });
    return;
  }
  if (item?.action === "scanDevice") {
    scanDevice();
    return;
  }
  if (!item?.route) return;
@@ -322,7 +366,7 @@
  allowedMenuTitles.value = titles;
  quickTools.value = quickToolSource.filter((item) =>
    titles.has(item.label)
    item.action === "scanDevice" || titles.has(item.label)
  );
}
src/pages/works.vue
@@ -656,6 +656,10 @@
    //     label: '设备台账',
    // },
    {
      icon: "/static/images/icon/saomashebei.svg",
      label: "扫码查设备",
    },
    {
      icon: "/static/images/icon/yunxingguanli.svg",
      label: "运行管理",
    },
@@ -672,6 +676,42 @@
      label: "设备巡检",
    },
  ]);
  // æ‰«ç æŸ¥è®¾å¤‡ï¼šè§£æž deviceId åŽè·³è½¬åˆ°è®¾å¤‡ä¿¡æ¯é¡µ
  const getDeviceIdFromScan = scanResult => {
    if (!scanResult) return null;
    const match = scanResult.match(/deviceId=(\d+)/);
    if (match && match[1]) return match[1];
    if (/^\d+$/.test(scanResult.trim())) return scanResult.trim();
    try {
      const obj = JSON.parse(scanResult);
      if (obj.deviceId) return String(obj.deviceId);
    } catch (e) {
      // å¿½ç•¥éž JSON å†…容
    }
    return null;
  };
  const startScanDevice = () => {
    uni.scanCode({
      scanType: ["qrCode", "barCode"],
      success: res => {
        console.log("扫码结果:", res.result);
        const deviceId = getDeviceIdFromScan(res.result);
        if (!deviceId) {
          uni.showToast({ title: "未识别到设备ID", icon: "none" });
          return;
        }
        uni.navigateTo({
          url: `/pages/equipmentManagement/deviceInfo/index?deviceId=${deviceId}`,
        });
      },
      fail: err => {
        console.error("扫码失败:", err);
        uni.showToast({ title: "扫码失败,请重试", icon: "none" });
      },
    });
  };
  // å¤„理常用功能点击
  const handleCommonItemClick = item => {
@@ -938,6 +978,9 @@
        uni.navigateTo({
          url: "/pages/equipmentManagement/ledger/index",
        });
        break;
      case "扫码查设备":
        startScanDevice();
        break;
      case "运行管理":
        uni.navigateTo({
@@ -1258,7 +1301,7 @@
    // æ”¶é›†æ‰€æœ‰æœ‰æƒé™çš„菜单标题(根据 meta.title)
    const allowedMenuTitles = new Set();
    const alwaysShowTitles = new Set(["采购退货单", "供应商管理"]);
    const alwaysShowTitles = new Set(["采购退货单", "供应商管理", "扫码查设备"]);
    const collectMenuTitles = routes => {
      if (!Array.isArray(routes)) return;
      routes.forEach(route => {
@@ -1284,6 +1327,7 @@
    // é€šç”¨è¿‡æ»¤å‡½æ•°
    const filterArray = (targetArray, specialMapping) => {
      const filtered = targetArray.filter(item => {
        if (alwaysShowTitles.has(item.label)) return true;
        let matched = allowedMenuTitles.has(item.label);
        if (specialMapping && !matched && specialMapping[item.label]) {
          matched = allowedMenuTitles.has(specialMapping[item.label]);
src/static/images/icon/saomashebei.svg
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" version="1.1" width="28" height="28" viewBox="0 0 28 28">
  <rect x="1" y="1" width="26" height="26" rx="6" fill="#9C27B0"/>
  <rect x="7" y="7" width="5" height="5" rx="1" fill="#FFFFFF"/>
  <rect x="16" y="7" width="5" height="5" rx="1" fill="#FFFFFF"/>
  <rect x="7" y="16" width="5" height="5" rx="1" fill="#FFFFFF"/>
  <rect x="16" y="16" width="2.5" height="2.5" rx="0.5" fill="#FFFFFF"/>
  <rect x="19.5" y="16" width="2.5" height="2.5" rx="0.5" fill="#FFFFFF"/>
  <rect x="16" y="19.5" width="2.5" height="2.5" rx="0.5" fill="#FFFFFF"/>
</svg>