Ver código fonte

设备台账样式

yanghao 1 dia atrás
pai
commit
6da7bbe983
2 arquivos alterados com 675 adições e 292 exclusões
  1. 666 292
      src/views/pms/device/DeviceInfo.vue
  2. 9 0
      src/views/pms/qhse/index.vue

+ 666 - 292
src/views/pms/device/DeviceInfo.vue

@@ -1,269 +1,267 @@
 <template>
-  <ContentWrap>
-    <div style="display: flex; flex-direction: row; height: 12em; margin-top: 2px">
-      <div style="flex: 1; height: 12em; margin-left: 20px">
-        <el-image
-          :key="index"
-          :src="defaultPicUrl"
-          style="width: 35em; height: 12em"
-          @click="imagePreview(defaultPicUrl)"
-          fit="contain"
-        />
+  <div class="device-info-page">
+    <ContentWrap class="device-shell device-shell--hero">
+      <div class="device-hero" v-loading="formLoading">
+        <div class="device-media-card">
+          <div class="device-media-glow"></div>
+          <el-image
+            :src="defaultPicUrl"
+            class="device-image"
+            @click="imagePreview(defaultPicUrl)"
+            fit="contain" />
+          <!-- <div class="device-image-caption">
+            <span class="device-image-tag">ASSET PREVIEW</span>
+            <span class="device-image-hint">点击查看原图</span>
+          </div> -->
+        </div>
+
+        <div class="device-hero-content">
+          <div class="device-hero-heading">
+            <div>
+              <div class="device-kicker">DEVICE PROFILE</div>
+              <h1 class="device-title">{{ displayText(formData.deviceName) }}</h1>
+              <p class="device-subtitle">
+                {{ displayText(formData.assetClassName) }} · {{ displayText(formData.deptName) }}
+              </p>
+            </div>
+            <div class="device-status-cluster">
+              <div class="device-status-pill" :class="statusToneClass">
+                <span class="device-status-dot"></span>
+                {{ deviceStatusText }}
+              </div>
+              <div class="device-meta-chip">
+                {{ t('devicePerson.assets') }} ·
+                {{
+                  displayText(getDictLabel(DICT_TYPE.PMS_ASSET_PROPERTY, formData.assetProperty))
+                }}
+              </div>
+            </div>
+          </div>
+
+          <div class="device-overview-grid">
+            <div class="overview-card overview-card--primary">
+              <span class="overview-label">{{ t('iotDevice.yfCode') }}</span>
+              <span class="overview-value">{{ displayText(formData.yfDeviceCode) }}</span>
+            </div>
+            <div class="overview-card">
+              <span class="overview-label">{{ t('iotDevice.code') }}</span>
+              <span class="overview-value">{{ displayText(formData.deviceCode) }}</span>
+            </div>
+            <div class="overview-card">
+              <span class="overview-label">{{ t('iotDevice.brand') }}</span>
+              <span class="overview-value">{{ displayText(formData.brandName) }}</span>
+            </div>
+            <div class="overview-card">
+              <span class="overview-label">{{ t('deviceForm.model') }}</span>
+              <span class="overview-value">{{ displayText(formData.model) }}</span>
+            </div>
+            <div class="overview-card">
+              <span class="overview-label">{{ t('info.deviceClass') }}</span>
+              <span class="overview-value">{{ displayText(formData.assetClassName) }}</span>
+            </div>
+            <div class="overview-card">
+              <span class="overview-label">{{ t('devicePerson.rp') }}</span>
+              <span class="overview-value">{{ displayText(formData.responsibleNames) }}</span>
+            </div>
+          </div>
+        </div>
       </div>
-      <div style="flex: 2; height: 12em; margin-top: 23px">
-        <el-form ref="formRef" :disabled="false" :model="formData" label-width="120px">
-          <el-row>
-            <el-col :span="8">
-              <el-form-item :label="t('iotDevice.yfCode')" prop="yfDeviceCode">
-                {{ formData.yfDeviceCode }}
-              </el-form-item>
-            </el-col>
-            <el-col :span="8">
-              <el-form-item :label="t('iotDevice.code')" prop="deviceCode">
-                {{ formData.deviceCode }}
-              </el-form-item>
-            </el-col>
-            <el-col :span="8">
-              <el-form-item :label="t('chooseMaintain.deviceName')" prop="deviceName">
-                {{ formData.deviceName }}
-              </el-form-item>
-            </el-col>
-            <el-col :span="8">
-              <el-form-item :label="t('iotDevice.brand')" prop="brand">
-                {{ formData.brandName }}
-              </el-form-item>
-            </el-col>
-            <el-col :span="8">
-              <el-form-item :label="t('iotDevice.dept')" prop="deptId">
-                {{ formData.deptName }}
-              </el-form-item>
-            </el-col>
-            <el-col :span="8">
-              <el-form-item :label="t('info.deviceClass')" prop="assetClass">
-                {{ formData.assetClassName }}
-              </el-form-item>
-            </el-col>
-            <el-col :span="8">
-              <el-form-item :label="t('monitor.status')" prop="deviceStatus">
-                {{ getDictLabel(DICT_TYPE.PMS_DEVICE_STATUS, formData.deviceStatus) }}
-              </el-form-item>
-            </el-col>
-            <el-col :span="8">
-              <el-form-item :label="t('devicePerson.assets')" prop="assetProperty">
-                {{ getDictLabel(DICT_TYPE.PMS_ASSET_PROPERTY, formData.assetProperty) }}
-              </el-form-item>
-            </el-col>
-            <el-col :span="8">
-              <el-form-item :label="t('deviceForm.model')" prop="model">
-                {{ formData.model ? formData.model : '-' }}
-              </el-form-item>
-            </el-col>
-            <el-col :span="8">
-              <el-form-item :label="t('devicePerson.rp')" prop="responsibleNames">
-                {{ formData.responsibleNames ? formData.responsibleNames : '-' }}
-              </el-form-item>
-            </el-col>
-          </el-row>
-        </el-form>
+    </ContentWrap>
+
+    <ContentWrap class="device-shell" v-loading="formLoading">
+      <div class="device-tabs-card">
+        <el-tabs v-model="activeName" class="device-tabs" @tab-click="handleTabClick">
+          <el-tab-pane :label="t('deviceInfo.basicInformation')" name="info">
+            <div class="info-panel">
+              <section class="info-section">
+                <div class="section-heading">
+                  <span class="section-index">01</span>
+                  <div>
+                    <h3>制造与交付</h3>
+                    <p>设备生产、供应与保修信息</p>
+                  </div>
+                </div>
+                <div class="detail-grid">
+                  <div class="detail-card">
+                    <span class="detail-label">{{ t('deviceInfo.mfg') }}</span>
+                    <span class="detail-value">{{ displayText(formData.zzName) }}</span>
+                  </div>
+                  <div class="detail-card">
+                    <span class="detail-label">{{ t('deviceInfo.pd') }}</span>
+                    <span class="detail-value">{{ displayDate(formData.manDate) }}</span>
+                  </div>
+                  <div class="detail-card">
+                    <span class="detail-label">{{ t('deviceInfo.supplier') }}</span>
+                    <span class="detail-value">{{ displayText(formData.supplierName) }}</span>
+                  </div>
+                  <div class="detail-card">
+                    <span class="detail-label">{{ t('deviceInfo.ni') }}</span>
+                    <span class="detail-value">{{ displayText(formData.nameplate) }}</span>
+                  </div>
+                  <div class="detail-card">
+                    <span class="detail-label">{{ t('deviceInfo.warranty') }}</span>
+                    <span class="detail-value">{{ displayDate(formData.expires) }}</span>
+                  </div>
+                </div>
+              </section>
+
+              <section class="info-section">
+                <div class="section-heading">
+                  <span class="section-index">02</span>
+                  <div>
+                    <h3>折旧与财务</h3>
+                    <p>采购价格、起始日期与折旧周期</p>
+                  </div>
+                </div>
+                <div class="detail-grid">
+                  <div class="detail-card">
+                    <span class="detail-label">{{ t('deviceInfo.pp') }}</span>
+                    <span class="detail-value">{{ displayText(formData.plPrice) }}</span>
+                  </div>
+                  <div class="detail-card">
+                    <span class="detail-label">{{ t('deviceInfo.pdate') }}</span>
+                    <span class="detail-value">{{ displayDate(formData.plDate) }}</span>
+                  </div>
+                  <div class="detail-card">
+                    <span class="detail-label">{{ t('deviceInfo.dp') }}</span>
+                    <span class="detail-value">{{ displayText(formData.plYear) }}</span>
+                  </div>
+                  <div class="detail-card">
+                    <span class="detail-label">{{ t('deviceInfo.ds') }}</span>
+                    <span class="detail-value">{{ displayDate(formData.plStartDate) }}</span>
+                  </div>
+                  <div class="detail-card">
+                    <span class="detail-label">{{ t('deviceInfo.yd') }}</span>
+                    <span class="detail-value">{{ displayText(formData.plMonthed) }}</span>
+                  </div>
+                  <div class="detail-card">
+                    <span class="detail-label">{{ t('deviceInfo.yy') }}</span>
+                    <span class="detail-value">{{ displayText(formData.plAmounted) }}</span>
+                  </div>
+                  <div class="detail-card">
+                    <span class="detail-label">{{ t('deviceInfo.sy') }}</span>
+                    <span class="detail-value">{{ displayText(formData.remainAmount) }}</span>
+                  </div>
+                  <div class="detail-card">
+                    <span class="detail-label">{{ t('deviceInfo.my') }}</span>
+                    <span class="detail-value">{{ displayText(formData.monthAmount) }}</span>
+                  </div>
+                  <div class="detail-card">
+                    <span class="detail-label">{{ t('deviceInfo.mmy') }}</span>
+                    <span class="detail-value">{{ displayText(formData.totalMonth) }}</span>
+                  </div>
+                  <div class="detail-card">
+                    <span class="detail-label">{{ t('deviceInfo.currency') }}</span>
+                    <span class="detail-value">{{ displayText(formData.currency) }}</span>
+                  </div>
+                </div>
+              </section>
+
+              <!-- <section class="info-section" v-if="list.length">
+                <div class="section-heading">
+                  <span class="section-index">03</span>
+                  <div>
+                    <h3>扩展字段</h3>
+                    <p>来自设备模板的补充属性</p>
+                  </div>
+                </div>
+                <div class="detail-grid">
+                  <div v-for="field in list" :key="field.sort" class="detail-card">
+                    <span class="detail-label">{{ field.name }}</span>
+                    <span class="detail-value">{{ displayText(field.value) }}</span>
+                  </div>
+                </div>
+              </section> -->
+            </div>
+          </el-tab-pane>
+          <el-tab-pane :label="t('deviceInfo.fileLibrary')" name="file">
+            <DeviceFile
+              ref="fileRef"
+              :deviceId="id"
+              :deviceName="formData.deviceName"
+              v-if="loadedTabs.includes('1')" />
+          </el-tab-pane>
+          <el-tab-pane :label="t('deviceInfo.deviceBOM')" name="bom">
+            <BomList
+              ref="bomRef"
+              v-model:activeName="activeName"
+              :deviceId="id"
+              :deviceCategoryName="formData.assetClassName"
+              v-if="loadedTabs.includes('2')" />
+          </el-tab-pane>
+          <el-tab-pane :label="t('deviceInfo.operationRecords')" name="record">
+            <RecordList
+              ref="recordRef"
+              v-model:activeName="activeName"
+              :deviceId="id"
+              v-if="loadedTabs.includes('3')" />
+          </el-tab-pane>
+          <el-tab-pane :label="t('deviceInfo.faultRecords')" name="failure">
+            <FailureList
+              ref="failureRef"
+              v-model:activeName="activeName"
+              :deviceId="id"
+              v-if="loadedTabs.includes('4')" />
+          </el-tab-pane>
+          <el-tab-pane :label="t('deviceInfo.repairRecords')" name="maintain">
+            <MaintainList
+              ref="maintainRef"
+              v-model:activeName="activeName"
+              :deviceId="id"
+              v-if="loadedTabs.includes('5')" />
+          </el-tab-pane>
+          <el-tab-pane :label="t('deviceInfo.maintenanceRecords')" name="maintenance">
+            <MaintenanceList
+              ref="maintenanceRef"
+              v-model:activeName="activeName"
+              :deviceId="id"
+              v-if="loadedTabs.includes('6')" />
+          </el-tab-pane>
+          <el-tab-pane :label="t('deviceInfo.inspectionRecords')" name="inspect">
+            <InspectList
+              ref="inspectRef"
+              v-model:activeName="activeName"
+              :deviceId="id"
+              v-if="loadedTabs.includes('7')" />
+          </el-tab-pane>
+          <el-tab-pane :label="t('deviceInfo.transferRecords')" name="allot">
+            <AllotLogList
+              ref="allotRef"
+              v-model:activeName="activeName"
+              :deviceId="id"
+              v-if="loadedTabs.includes('8')" />
+          </el-tab-pane>
+          <el-tab-pane :label="t('deviceInfo.statusChangeRecords')" name="status">
+            <DeviceStatusLogList
+              ref="statusRef"
+              v-model:activeName="activeName"
+              :deviceId="id"
+              v-if="loadedTabs.includes('9')" />
+          </el-tab-pane>
+          <el-tab-pane :label="t('deviceInfo.RPAdjustmentRecords')" name="person">
+            <PersonList
+              ref="personRef"
+              v-model:activeName="activeName"
+              :deviceId="id"
+              v-if="loadedTabs.includes('10')" />
+          </el-tab-pane>
+          <el-tab-pane :label="t('deviceInfo.associationDevice')" name="association">
+            <AssociationDevices
+              ref="associationRef"
+              v-model:activeName="activeName"
+              :deviceId="id"
+              v-if="loadedTabs.includes('11')" />
+          </el-tab-pane>
+        </el-tabs>
       </div>
-    </div>
-  </ContentWrap>
-  <ContentWrap v-loading="formLoading">
-    <el-tabs v-model="activeName" @tab-click="handleTabClick">
-      <el-tab-pane :label="t('deviceInfo.basicInformation')" name="info">
-        <el-form style="margin-top: 5px; margin-left: 35px; margin-right: 35px">
-          <el-row style="border-bottom: 1px solid #dcdfe6">
-            <el-col :span="6">
-              <el-form-item :label="t('deviceInfo.mfg')" prop="manufacturerId">
-                {{ formData.zzName }}
-              </el-form-item>
-            </el-col>
-            <el-col :span="6">
-              <el-form-item :label="t('deviceInfo.pd')" prop="manDate">
-                {{ formatDate(formData.manDate, 'YYYY-MM-DD') }}
-              </el-form-item>
-            </el-col>
-            <el-col :span="6">
-              <el-form-item :label="t('deviceInfo.supplier')" prop="supplierId">
-                {{ formData.supplierName ? formData.supplierName : '-' }}
-              </el-form-item>
-            </el-col>
-            <el-col :span="6">
-              <el-form-item :label="t('deviceInfo.ni')" prop="nameplate">
-                {{ formData.nameplate ? formData.nameplate : '-' }}
-              </el-form-item>
-            </el-col>
-            <el-col :span="6">
-              <el-form-item :label="t('deviceInfo.warranty')" prop="expires">
-                {{ formData.expires ? formatDate(formData.expires, 'YYYY-MM-DD') : '-' }}
-              </el-form-item>
-            </el-col>
-          </el-row>
-          <el-row style="margin-top: 20px; border-bottom: 1px solid #dcdfe6">
-            <el-col :span="6">
-              <el-form-item :label="t('deviceInfo.pp')" prop="plPrice">
-                {{ formData.plPrice ? formData.plPrice : '-' }}
-              </el-form-item>
-            </el-col>
-            <el-col :span="6">
-              <el-form-item :label="t('deviceInfo.pdate')" prop="plDate">
-                {{ formData.plDate ? formatDate(formData.plDate, 'YYYY-MM-DD') : '-' }}
-              </el-form-item>
-            </el-col>
-            <el-col :span="6">
-              <el-form-item :label="t('deviceInfo.dp')" prop="plYear">
-                {{ formData.plYear ? formData.plYear : '-' }}
-              </el-form-item>
-            </el-col>
-            <el-col :span="6">
-              <el-form-item :label="t('deviceInfo.ds')" prop="plStartDate">
-                {{ formData.plStartDate ? formatDate(formData.plStartDate, 'YYYY-MM-DD') : '-' }}
-              </el-form-item>
-            </el-col>
-            <el-col :span="6">
-              <el-form-item :label="t('deviceInfo.yd')" prop="plMonthed">
-                {{ formData.plMonthed ? formData.plMonthed : '-' }}
-              </el-form-item>
-            </el-col>
-            <el-col :span="6">
-              <el-form-item :label="t('deviceInfo.yy')" prop="plAmounted">
-                {{ formData.plAmounted ? formData.plAmounted : '-' }}
-              </el-form-item>
-            </el-col>
-            <el-col :span="6">
-              <el-form-item :label="t('deviceInfo.sy')" prop="remainAmount">
-                {{ formData.remainAmount ? formData.remainAmount : '-' }}
-              </el-form-item>
-            </el-col>
-
-            <el-col :span="6">
-              <el-form-item :label="t('deviceInfo.my')" prop="monthAmount">
-                {{ formData.monthAmount ? formData.monthAmount : '-' }}
-              </el-form-item>
-            </el-col>
-
-            <el-col :span="6">
-              <el-form-item :label="t('deviceInfo.mmy')" prop="totalMonth">
-                {{ formData.totalMonth ? formData.totalMonth : '-' }}
-              </el-form-item>
-            </el-col>
-
-            <el-col :span="6">
-              <el-form-item :label="t('deviceInfo.currency')" prop="currency">
-                {{ formData.currency ? formData.currency : '-' }}
-              </el-form-item>
-            </el-col>
-          </el-row>
-          <el-row style="margin-top: 20px">
-            <el-col v-for="field in list" :key="field.sort" :span="6">
-              <el-form-item :label="field.name" :prop="field.identifier">
-                {{ field.value }}
-              </el-form-item>
-            </el-col>
-          </el-row>
-        </el-form>
-      </el-tab-pane>
-      <el-tab-pane :label="t('deviceInfo.fileLibrary')" name="file">
-        <!--        <DeviceUpload ref="fileRef" v-if="loadedTabs.includes('1')" />-->
-        <DeviceFile
-          ref="fileRef"
-          :deviceId="id"
-          :deviceName="formData.deviceName"
-          v-if="loadedTabs.includes('1')"
-        />
-      </el-tab-pane>
-      <el-tab-pane :label="t('deviceInfo.deviceBOM')" name="bom">
-        <BomList
-          ref="bomRef"
-          v-model:activeName="activeName"
-          :deviceId="id"
-          :deviceCategoryName="formData.assetClassName"
-          v-if="loadedTabs.includes('2')"
-        />
-      </el-tab-pane>
-      <el-tab-pane :label="t('deviceInfo.operationRecords')" name="record">
-        <RecordList
-          ref="recordRef"
-          v-model:activeName="activeName"
-          :deviceId="id"
-          v-if="loadedTabs.includes('3')"
-        />
-      </el-tab-pane>
-      <el-tab-pane :label="t('deviceInfo.faultRecords')" name="failure">
-        <FailureList
-          ref="failureRef"
-          v-model:activeName="activeName"
-          :deviceId="id"
-          v-if="loadedTabs.includes('4')"
-        />
-      </el-tab-pane>
-      <el-tab-pane :label="t('deviceInfo.repairRecords')" name="maintain">
-        <MaintainList
-          ref="maintainRef"
-          v-model:activeName="activeName"
-          :deviceId="id"
-          v-if="loadedTabs.includes('5')"
-        />
-      </el-tab-pane>
-      <el-tab-pane :label="t('deviceInfo.maintenanceRecords')" name="maintenance">
-        <MaintenanceList
-          ref="maintenanceRef"
-          v-model:activeName="activeName"
-          :deviceId="id"
-          v-if="loadedTabs.includes('6')"
-        />
-      </el-tab-pane>
-      <el-tab-pane :label="t('deviceInfo.inspectionRecords')" name="inspect">
-        <InspectList
-          ref="inspectRef"
-          v-model:activeName="activeName"
-          :deviceId="id"
-          v-if="loadedTabs.includes('7')"
-        />
-      </el-tab-pane>
-      <el-tab-pane :label="t('deviceInfo.transferRecords')" name="allot">
-        <AllotLogList
-          ref="allotRef"
-          v-model:activeName="activeName"
-          :deviceId="id"
-          v-if="loadedTabs.includes('8')"
-        />
-      </el-tab-pane>
-      <el-tab-pane :label="t('deviceInfo.statusChangeRecords')" name="status">
-        <DeviceStatusLogList
-          ref="statusRef"
-          v-model:activeName="activeName"
-          :deviceId="id"
-          v-if="loadedTabs.includes('9')"
-        />
-      </el-tab-pane>
-      <el-tab-pane :label="t('deviceInfo.RPAdjustmentRecords')" name="person">
-        <PersonList
-          ref="personRef"
-          v-model:activeName="activeName"
-          :deviceId="id"
-          v-if="loadedTabs.includes('10')"
-        />
-      </el-tab-pane>
-
-      <!-- 关联设备 -->
-      <el-tab-pane :label="t('deviceInfo.associationDevice')" name="association">
-        <AssociationDevices
-          ref="personRef"
-          v-model:activeName="activeName"
-          :deviceId="id"
-          v-if="loadedTabs.includes('11')"
-        />
-      </el-tab-pane>
-    </el-tabs>
-  </ContentWrap>
+    </ContentWrap>
+  </div>
 </template>
+
 <script lang="ts" setup>
 import { IotDeviceApi, IotDeviceVO } from '@/api/pms/device'
 import { DICT_TYPE, getDictLabel } from '@/utils/dict'
 import { formatDate } from '../../../utils/formatTime'
-import DeviceUpload from '@/views/pms/device/DeviceUpload.vue'
-import BomInfo from '@/views/pms/device/bom/BomInfo.vue'
 import DeviceFile from '@/views/pms/device/DeviceFile.vue'
 import BomList from '@/views/pms/device/bom/BomList.vue'
 import FailureList from '@/views/pms/device/FailureList.vue'
@@ -276,25 +274,23 @@ import PersonList from '@/views/pms/device/personlog/PersonList.vue'
 import RecordList from '@/views/pms/device/record/RecordList.vue'
 import AssociationDevices from '@/views/pms/device/completeSet/AssociationDevices.vue'
 import { createImageViewer } from '@/components/ImageViewer'
-import { ref, onMounted } from 'vue'
+import { computed, onMounted, ref } from 'vue'
 import { getAccessToken } from '@/utils/auth'
 
 const defaultPicUrl = ref(
   import.meta.env.VITE_BASE_URL + '/admin-api/infra/file/29/get/IntegratedSolution.png'
-) // 默认设备图片
+)
 
 defineOptions({ name: 'DeviceDetailInfo' })
 
-const { t } = useI18n() // 国际化
-const message = useMessage() // 消息弹窗
-const { params } = useRoute() // 查询参数
-const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
-const activeName = ref('info') // Tag 激活的窗口
-const list = ref([])
+const { t } = useI18n()
+const { params } = useRoute()
+const formLoading = ref(false)
+const activeName = ref('info')
+const list = ref<any[]>([])
 const id = params.id
-const fileRef = ref() // 搜索的表单
-// SPU 表单数据
-const formData = ref({
+const fileRef = ref()
+const formData = ref<any>({
   id: undefined,
   code: undefined,
   name: undefined,
@@ -317,29 +313,43 @@ const formData = ref({
   totalMonth: undefined,
   currency: undefined
 })
-const pics = ref([])
-const imgSrc = ref('')
-const loadedTabs = ref(['info']) // 记录已加载的标签
+const loadedTabs = ref(['info'])
+
+const deviceStatusText = computed(() =>
+  displayText(getDictLabel(DICT_TYPE.PMS_DEVICE_STATUS, formData.value.deviceStatus))
+)
+
+const statusToneClass = computed(() => {
+  const status = String(formData.value.deviceStatus ?? '')
+  if (status === '1' || status.toLowerCase() === 'online') return 'is-good'
+  if (status === '0' || status.toLowerCase() === 'offline') return 'is-muted'
+  return 'is-warning'
+})
+
+function displayText(value: unknown) {
+  return value === undefined || value === null || value === '' ? '-' : String(value)
+}
+
+function displayDate(value: unknown) {
+  return value ? formatDate(value, 'YYYY-MM-DD') : '-'
+}
 
-/** 获得详情 */
 const getDetail = async () => {
-  if (id) {
-    formLoading.value = true
-    try {
-      const res = (await IotDeviceApi.getIotDevice(id)) as IotDeviceVO
-      formData.value = res
-      pics.value.push(res.picUrl)
-      if (res) {
-        if (res.templateJson) {
-          list.value = JSON.parse(res.templateJson)
-        }
-      }
-      if (formData.value.picUrl) {
-        defaultPicUrl.value = formData.value.picUrl
-      }
-    } finally {
-      formLoading.value = false
+  if (!id) return
+  formLoading.value = true
+  try {
+    const res = (await IotDeviceApi.getIotDevice(id)) as IotDeviceVO
+    formData.value = res
+    if (res?.templateJson) {
+      list.value = JSON.parse(res.templateJson)
+    } else {
+      list.value = []
+    }
+    if (res?.picUrl) {
+      defaultPicUrl.value = res.picUrl
     }
+  } finally {
+    formLoading.value = false
   }
 }
 
@@ -355,14 +365,378 @@ const imagePreview = (imgUrl: string) => {
 
 const handleTabClick = (tab) => {
   if (!loadedTabs.value.includes(tab.index)) {
-    // 这里可以添加每个标签对应的加载逻辑,如果有的话
     loadedTabs.value.push(tab.index)
   }
 }
 
-/** 初始化 */
 onMounted(async () => {
   await getDetail()
 })
 </script>
-<style scoped></style>
+
+<style scoped>
+.device-info-page {
+  --device-ink: #20304d;
+  --device-muted: #6f809e;
+  display: grid;
+  gap: 16px;
+}
+
+:deep(.device-shell .el-card),
+:deep(.device-shell > .el-card) {
+  border: 1px solid rgba(209, 222, 240, 0.92);
+  border-radius: 24px;
+  overflow: hidden;
+  background: radial-gradient(circle at top left, rgba(162, 193, 248, 0.18), transparent 30%),
+    linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(244, 248, 255, 0.98) 100%);
+  box-shadow:
+    inset 0 1px 0 rgba(255, 255, 255, 0.96),
+    inset 0 -18px 30px rgba(214, 228, 245, 0.18),
+    0 20px 50px rgba(35, 61, 106, 0.08);
+}
+
+:deep(.device-shell .el-card__body) {
+  padding: 0;
+}
+
+.device-hero {
+  display: grid;
+  grid-template-columns: minmax(280px, 360px) minmax(0, 1fr);
+  gap: 24px;
+  padding: 26px;
+  min-width: 0;
+}
+
+.device-media-card {
+  position: relative;
+  min-height: 280px;
+  border-radius: 22px;
+  overflow: hidden;
+  background: radial-gradient(circle at 20% 15%, rgba(170, 201, 249, 0.28), transparent 28%),
+    linear-gradient(160deg, #edf4ff 0%, #ffffff 56%, #edf4ff 100%);
+  border: 1px solid rgba(206, 221, 241, 0.94);
+  box-shadow:
+    inset 0 1px 0 rgba(255, 255, 255, 0.98),
+    inset 0 -18px 26px rgba(205, 221, 244, 0.22),
+    0 18px 36px rgba(55, 88, 140, 0.08);
+}
+
+.device-media-glow {
+  position: absolute;
+  inset: auto -20% -24% auto;
+  width: 220px;
+  height: 220px;
+  border-radius: 999px;
+  background: radial-gradient(circle, rgba(137, 176, 242, 0.28) 0%, transparent 68%);
+  pointer-events: none;
+}
+
+.device-image {
+  width: 100%;
+  height: 100%;
+  min-height: 280px;
+  padding: 26px;
+  cursor: zoom-in;
+}
+
+.device-image-caption {
+  position: absolute;
+  left: 20px;
+  right: 20px;
+  bottom: 18px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+  padding: 10px 14px;
+  border-radius: 16px;
+  border: 1px solid rgba(213, 225, 241, 0.96);
+  background: rgba(255, 255, 255, 0.72);
+  backdrop-filter: blur(10px);
+  box-shadow: 0 10px 24px rgba(41, 74, 126, 0.08);
+}
+
+.device-image-tag,
+.device-image-hint {
+  font-family: 'Georgia', 'Times New Roman', serif;
+  letter-spacing: 0.08em;
+  font-size: 12px;
+  color: var(--device-muted);
+}
+
+.device-hero-content {
+  display: grid;
+  gap: 22px;
+  min-width: 0;
+}
+
+.device-hero-heading {
+  display: flex;
+  align-items: flex-start;
+  justify-content: space-between;
+  gap: 16px;
+}
+
+.device-kicker {
+  font-family: 'Georgia', 'Times New Roman', serif;
+  font-size: 12px;
+  letter-spacing: 0.18em;
+  text-transform: uppercase;
+  color: #7d8fae;
+}
+
+.device-title {
+  margin: 10px 0 6px;
+  font-family: 'Georgia', 'Times New Roman', serif;
+  font-size: clamp(28px, 3vw, 42px);
+  line-height: 1.05;
+  color: #1c2c48;
+}
+
+.device-subtitle {
+  margin: 0;
+  color: var(--device-muted);
+  font-size: 14px;
+}
+
+.device-status-cluster {
+  display: grid;
+  gap: 10px;
+  justify-items: end;
+}
+
+.device-status-pill,
+.device-meta-chip {
+  display: inline-flex;
+  align-items: center;
+  gap: 8px;
+  padding: 10px 14px;
+  border-radius: 999px;
+  border: 1px solid rgba(209, 221, 239, 0.96);
+  background: rgba(255, 255, 255, 0.86);
+  box-shadow:
+    inset 0 1px 0 rgba(255, 255, 255, 0.92),
+    0 8px 18px rgba(44, 72, 119, 0.06);
+  color: #344766;
+  white-space: nowrap;
+}
+
+.device-status-pill.is-good {
+  color: #28575c;
+}
+
+.device-status-pill.is-muted {
+  color: #54657e;
+}
+
+.device-status-pill.is-warning {
+  color: #805d28;
+}
+
+.device-status-dot {
+  width: 9px;
+  height: 9px;
+  border-radius: 50%;
+  background: currentColor;
+  box-shadow: 0 0 0 4px rgb(93 121 183 / 12%);
+}
+
+.device-overview-grid {
+  display: grid;
+  grid-template-columns: repeat(3, minmax(0, 1fr));
+  gap: 14px;
+}
+
+.overview-card {
+  display: grid;
+  gap: 10px;
+  min-height: 108px;
+  padding: 18px;
+  border-radius: 18px;
+  border: 1px solid rgba(214, 225, 241, 0.92);
+  background: linear-gradient(180deg, rgba(255, 255, 255, 0.94) 0%, rgba(243, 248, 255, 0.98) 100%);
+  box-shadow:
+    inset 0 1px 0 rgba(255, 255, 255, 0.94),
+    inset 0 -10px 20px rgba(214, 226, 244, 0.18);
+}
+
+.overview-card--primary {
+  background: radial-gradient(circle at top right, rgba(156, 189, 242, 0.2), transparent 28%),
+    linear-gradient(180deg, rgba(255, 255, 255, 0.96) 0%, rgba(240, 246, 255, 1) 100%);
+}
+
+.overview-label {
+  font-size: 12px;
+  letter-spacing: 0.1em;
+  text-transform: uppercase;
+  color: #7d8ea9;
+}
+
+.overview-value {
+  font-size: 18px;
+  line-height: 1.35;
+  color: var(--device-ink);
+  word-break: break-word;
+}
+
+.device-tabs-card {
+  padding: 18px 20px 22px;
+}
+
+:deep(.device-tabs .el-tabs__header) {
+  margin: 0 0 18px;
+}
+
+:deep(.device-tabs .el-tabs__nav-wrap::after) {
+  height: 1px;
+  background: rgba(213, 225, 241, 0.9);
+}
+
+:deep(.device-tabs .el-tabs__item) {
+  height: 42px;
+  padding: 0 16px;
+  color: #667892;
+  font-weight: 600;
+}
+
+:deep(.device-tabs .el-tabs__item.is-active) {
+  color: #29456d;
+}
+
+:deep(.device-tabs .el-tabs__active-bar) {
+  height: 3px;
+  border-radius: 999px;
+  background: linear-gradient(90deg, #8db3f4 0%, #597fca 100%);
+}
+
+.info-panel {
+  display: grid;
+  gap: 18px;
+}
+
+.info-section {
+  padding: 22px;
+  border-radius: 22px;
+  border: 1px solid rgba(210, 223, 241, 0.92);
+  background: radial-gradient(circle at top left, rgba(164, 193, 242, 0.12), transparent 30%),
+    linear-gradient(180deg, rgba(255, 255, 255, 0.96) 0%, rgba(244, 248, 255, 0.98) 100%);
+  box-shadow:
+    inset 0 1px 0 rgba(255, 255, 255, 0.94),
+    inset 0 -12px 22px rgba(213, 226, 243, 0.14);
+}
+
+.section-heading {
+  display: flex;
+  align-items: center;
+  gap: 14px;
+  margin-bottom: 18px;
+}
+
+.section-index {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  width: 46px;
+  height: 46px;
+  border-radius: 14px;
+  background: linear-gradient(180deg, #ffffff 0%, #eaf2ff 100%);
+  border: 1px solid rgba(205, 219, 240, 0.95);
+  color: #5a79b4;
+  font-family: 'Georgia', 'Times New Roman', serif;
+  font-size: 18px;
+  box-shadow:
+    inset 0 1px 0 rgba(255, 255, 255, 0.96),
+    0 8px 16px rgba(55, 87, 138, 0.06);
+}
+
+.section-heading h3 {
+  margin: 0 0 4px;
+  font-family: 'Georgia', 'Times New Roman', serif;
+  font-size: 22px;
+  color: #223453;
+}
+
+.section-heading p {
+  margin: 0;
+  font-size: 13px;
+  color: var(--device-muted);
+}
+
+.detail-grid {
+  display: grid;
+  grid-template-columns: repeat(4, minmax(0, 1fr));
+  gap: 14px;
+}
+
+.detail-card {
+  display: grid;
+  gap: 10px;
+  min-height: 98px;
+  padding: 16px;
+  border-radius: 18px;
+  border: 1px solid rgba(214, 226, 243, 0.95);
+  background: rgba(255, 255, 255, 0.76);
+  box-shadow:
+    inset 0 1px 0 rgba(255, 255, 255, 0.94),
+    inset 0 -8px 16px rgba(215, 227, 244, 0.14);
+}
+
+.detail-label {
+  font-size: 12px;
+  letter-spacing: 0.06em;
+  text-transform: uppercase;
+  color: #7d8fa9;
+}
+
+.detail-value {
+  color: var(--device-ink);
+  font-size: 16px;
+  line-height: 1.45;
+  word-break: break-word;
+}
+
+@media (max-width: 1440px) {
+  .device-overview-grid,
+  .detail-grid {
+    grid-template-columns: repeat(3, minmax(0, 1fr));
+  }
+}
+
+@media (max-width: 1080px) {
+  .device-hero {
+    grid-template-columns: 1fr;
+  }
+
+  .device-status-cluster {
+    justify-items: start;
+  }
+
+  .device-overview-grid,
+  .detail-grid {
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+  }
+}
+
+@media (max-width: 768px) {
+  .device-hero {
+    padding: 18px;
+  }
+
+  .device-hero-heading {
+    flex-direction: column;
+  }
+
+  .device-tabs-card {
+    padding: 14px;
+  }
+
+  .info-section {
+    padding: 18px;
+  }
+
+  .device-overview-grid,
+  .detail-grid {
+    grid-template-columns: 1fr;
+  }
+}
+</style>

+ 9 - 0
src/views/pms/qhse/index.vue

@@ -26,6 +26,14 @@
             @keyup.enter="handleQuery"
             class="!w-120px" />
         </el-form-item>
+        <el-form-item label="编码" prop="measureCode">
+          <el-input
+            v-model="queryParams.measureCode"
+            placeholder="请输入计量器具编码"
+            clearable
+            @keyup.enter="handleQuery"
+            class="!w-120px" />
+        </el-form-item>
         <el-form-item label="是否过期" prop="expired">
           <el-select
             v-model="queryParams.expired"
@@ -473,6 +481,7 @@ const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
   measureName: undefined,
+  measureCertNo: undefined,
   deptId: undefined,
   expired: undefined,
   alertWarn: undefined