Parcourir la source

feat(device): 完善设备详情信息展示

- 补齐基础、制造及财务信息字段
- 转换编码分类名称并加载动态字段
- 优化六列布局和设备图片展示
Zimo il y a 1 semaine
Parent
commit
4f5f6917a1
2 fichiers modifiés avec 182 ajouts et 44 suppressions
  1. 30 23
      src/views/pms/device/DeviceBasicInfo.vue
  2. 152 21
      src/views/pms/device/DeviceInfo.vue

+ 30 - 23
src/views/pms/device/DeviceBasicInfo.vue

@@ -7,7 +7,7 @@
         </span>
         </span>
         <div>
         <div>
           <h3>制造信息</h3>
           <h3>制造信息</h3>
-          <p>设备生产、供应及质保信息</p>
+          <p>设备生产、供应、质保及启用信息</p>
         </div>
         </div>
       </div>
       </div>
       <div class="basic-grid basic-grid--manufacture">
       <div class="basic-grid basic-grid--manufacture">
@@ -24,8 +24,8 @@
           <Icon icon="ep:coin" />
           <Icon icon="ep:coin" />
         </span>
         </span>
         <div>
         <div>
-          <h3>资产折旧</h3>
-          <p>采购成本及折旧执行信息</p>
+          <h3>财务信息</h3>
+          <p>{{ financeDescription }}</p>
         </div>
         </div>
       </div>
       </div>
       <div class="basic-grid">
       <div class="basic-grid">
@@ -42,7 +42,7 @@
           <Icon icon="ep:grid" />
           <Icon icon="ep:grid" />
         </span>
         </span>
         <div>
         <div>
-          <h3>扩展属性</h3>
+          <h3>其他信息</h3>
           <p>设备分类模板补充信息</p>
           <p>设备分类模板补充信息</p>
         </div>
         </div>
       </div>
       </div>
@@ -66,7 +66,9 @@ interface DeviceBasicInfoData {
   manDate?: number | string | Date
   manDate?: number | string | Date
   supplierName?: string
   supplierName?: string
   expires?: number | string | Date
   expires?: number | string | Date
+  enableDate?: number | string | Date
   nameplate?: string
   nameplate?: string
+  assetProperty?: string
   plPrice?: number | string
   plPrice?: number | string
   plDate?: number | string | Date
   plDate?: number | string | Date
   plYear?: number | string
   plYear?: number | string
@@ -82,7 +84,9 @@ interface DeviceBasicInfoData {
 interface TemplateField {
 interface TemplateField {
   sort?: number
   sort?: number
   name?: string
   name?: string
-  value?: string | number | null
+  code?: string
+  type?: string
+  value?: string | number | Date | null
   identifier?: string
   identifier?: string
 }
 }
 
 
@@ -124,32 +128,42 @@ const manufactureFields = computed<DisplayField[]>(() => [
   {
   {
     label: '质保到期',
     label: '质保到期',
     value: displayDate(props.device.expires)
     value: displayDate(props.device.expires)
+  },
+  {
+    label: '启用日期',
+    value: displayDate(props.device.enableDate)
   }
   }
 ])
 ])
 
 
+const isOwnedAsset = computed(() => props.device.assetProperty !== 'zl')
+
+const financeDescription = computed(() =>
+  isOwnedAsset.value ? '采购成本及折旧执行信息' : '租赁成本及租赁执行信息'
+)
+
 const financeFields = computed<DisplayField[]>(() => [
 const financeFields = computed<DisplayField[]>(() => [
   {
   {
-    label: '采购价格',
+    label: isOwnedAsset.value ? '采购价格' : '租赁价格',
     value: displayValue(props.device.plPrice)
     value: displayValue(props.device.plPrice)
   },
   },
   {
   {
-    label: '采购日期',
+    label: isOwnedAsset.value ? '采购日期' : '租赁日期',
     value: displayDate(props.device.plDate)
     value: displayDate(props.device.plDate)
   },
   },
   {
   {
-    label: '折旧年限',
+    label: isOwnedAsset.value ? '折旧年限' : '租赁年限',
     value: displayValue(props.device.plYear)
     value: displayValue(props.device.plYear)
   },
   },
   {
   {
-    label: '折旧开始日期',
+    label: isOwnedAsset.value ? '折旧开始日期' : '租赁开始日期',
     value: displayDate(props.device.plStartDate)
     value: displayDate(props.device.plStartDate)
   },
   },
   {
   {
-    label: '已提折旧月数',
+    label: isOwnedAsset.value ? '已提折旧月数' : '已租赁月数',
     value: displayValue(props.device.plMonthed)
     value: displayValue(props.device.plMonthed)
   },
   },
   {
   {
-    label: '已提折旧金额',
+    label: isOwnedAsset.value ? '已提折旧金额' : '已租赁金额',
     value: displayValue(props.device.plAmounted)
     value: displayValue(props.device.plAmounted)
   },
   },
   {
   {
@@ -172,9 +186,12 @@ const financeFields = computed<DisplayField[]>(() => [
 
 
 const normalizedTemplateFields = computed(() => {
 const normalizedTemplateFields = computed(() => {
   return (props.templateFields || []).map((field, index) => ({
   return (props.templateFields || []).map((field, index) => ({
-    key: field.identifier || field.name || String(field.sort ?? index),
+    key: field.code || field.identifier || field.name || String(field.sort ?? index),
     label: field.name || '-',
     label: field.name || '-',
-    value: displayValue(field.value)
+    value:
+      field.type === 'date'
+        ? displayDate(field.value as number | string | Date | undefined)
+        : displayValue(field.value as string | number | null | undefined)
   }))
   }))
 })
 })
 </script>
 </script>
@@ -290,10 +307,6 @@ const normalizedTemplateFields = computed(() => {
     &:nth-child(3n) {
     &:nth-child(3n) {
       border-right: 0;
       border-right: 0;
     }
     }
-
-    &:nth-child(4) {
-      grid-column: span 2;
-    }
   }
   }
 }
 }
 
 
@@ -329,12 +342,6 @@ const normalizedTemplateFields = computed(() => {
       border-right: 0;
       border-right: 0;
     }
     }
   }
   }
-
-  .basic-grid--manufacture {
-    .basic-item:nth-child(4) {
-      grid-column: span 1;
-    }
-  }
 }
 }
 
 
 @media (width <= 575px) {
 @media (width <= 575px) {

+ 152 - 21
src/views/pms/device/DeviceInfo.vue

@@ -3,7 +3,7 @@
     <div class="device-main">
     <div class="device-main">
       <div class="device-media">
       <div class="device-media">
         <button class="device-image-frame" type="button" @click="imagePreview(defaultPicUrl)">
         <button class="device-image-frame" type="button" @click="imagePreview(defaultPicUrl)">
-          <el-image :src="defaultPicUrl" class="device-image" fit="cover">
+          <el-image :src="defaultPicUrl" class="device-image" fit="contain">
             <template #error>
             <template #error>
               <div class="device-image__empty">
               <div class="device-image__empty">
                 <Icon icon="ep:picture" />
                 <Icon icon="ep:picture" />
@@ -39,7 +39,7 @@
               <Icon :icon="field.icon" />
               <Icon :icon="field.icon" />
               {{ field.label }}
               {{ field.label }}
             </span>
             </span>
-            <strong>{{ field.value }}</strong>
+            <strong :title="field.value">{{ field.value }}</strong>
           </div>
           </div>
         </div>
         </div>
       </div>
       </div>
@@ -103,6 +103,8 @@
 <script setup lang="ts">
 <script setup lang="ts">
 import { computed, onMounted, ref } from 'vue'
 import { computed, onMounted, ref } from 'vue'
 import { IotDeviceApi, IotDeviceVO } from '@/api/pms/device'
 import { IotDeviceApi, IotDeviceVO } from '@/api/pms/device'
+import { DeviceAttrModelApi, type DeviceAttrModelData } from '@/api/pms/deviceattrmodel'
+import { IotYfClassifyApi } from '@/api/pms/yfclass'
 import { createImageViewer } from '@/components/ImageViewer'
 import { createImageViewer } from '@/components/ImageViewer'
 import { getAccessToken } from '@/utils/auth'
 import { getAccessToken } from '@/utils/auth'
 import { DICT_TYPE, getDictLabel } from '@/utils/dict'
 import { DICT_TYPE, getDictLabel } from '@/utils/dict'
@@ -139,11 +141,18 @@ type DeviceDetail = Partial<
     | 'totalMonth'
     | 'totalMonth'
   >
   >
 > & {
 > & {
+  yfClass?: string
   yfDeviceCode?: string
   yfDeviceCode?: string
   brandName?: string
   brandName?: string
   assetClass?: number
   assetClass?: number
   assetClassName?: string
   assetClassName?: string
   model?: string | number
   model?: string | number
+  carNo?: string
+  deviceNo?: string
+  useProject?: string
+  assetOwnership?: string
+  address?: string
+  remark?: string
   responsibleNames?: string
   responsibleNames?: string
   devicePersons?: string
   devicePersons?: string
   zzName?: string
   zzName?: string
@@ -163,13 +172,18 @@ type DeviceDetail = Partial<
   monthAmount?: number | string
   monthAmount?: number | string
   totalMonth?: number | string
   totalMonth?: number | string
   currency?: string
   currency?: string
-  templateJson?: string
+  templateJson?: string | TemplateField[]
+  assetProperty?: string
+  [key: string]: unknown
 }
 }
 
 
 interface TemplateField {
 interface TemplateField {
   sort?: number
   sort?: number
   name?: string
   name?: string
-  value?: string | number | null
+  code?: string
+  type?: string
+  defaultValue?: string
+  value?: string | number | Date | null
   identifier?: string
   identifier?: string
 }
 }
 
 
@@ -180,6 +194,12 @@ interface MainInfoField {
   icon: string
   icon: string
 }
 }
 
 
+interface EncodingTypeOption {
+  label: string
+  value: string
+  children?: EncodingTypeOption[]
+}
+
 const defaultDevicePic =
 const defaultDevicePic =
   import.meta.env.VITE_BASE_URL + '/admin-api/infra/file/29/get/IntegratedSolution.png'
   import.meta.env.VITE_BASE_URL + '/admin-api/infra/file/29/get/IntegratedSolution.png'
 
 
@@ -190,6 +210,8 @@ const formLoading = ref(false)
 const activeName = ref('info')
 const activeName = ref('info')
 const defaultPicUrl = ref(defaultDevicePic)
 const defaultPicUrl = ref(defaultDevicePic)
 const formData = ref<DeviceDetail>({})
 const formData = ref<DeviceDetail>({})
+const encodingTypeOptions = ref<EncodingTypeOption[]>([])
+const templateFields = ref<TemplateField[]>([])
 
 
 const deviceId = computed(() => {
 const deviceId = computed(() => {
   const id = params.id ?? query.id
   const id = params.id ?? query.id
@@ -217,18 +239,87 @@ const responsibleNames = computed(() => {
   return formData.value.responsibleNames || formData.value.devicePersons || '-'
   return formData.value.responsibleNames || formData.value.devicePersons || '-'
 })
 })
 
 
-const templateFields = computed<TemplateField[]>(() => {
-  if (!formData.value.templateJson) return []
+const yfClassLabel = computed(() => {
+  if (!formData.value.yfClass) return '-'
+
+  const codes = formData.value.yfClass.split(',').filter(Boolean)
+  const labels: string[] = []
+  let options = encodingTypeOptions.value
+
+  for (const code of codes) {
+    const option = options.find((item) => String(item.value) === code)
+    if (!option) return '-'
+    labels.push(option.label)
+    options = option.children || []
+  }
+
+  return labels.join(' / ') || '-'
+})
+
+const getEncodingTypeOptions = async () => {
+  const options = await IotYfClassifyApi.getChildrenList()
+  encodingTypeOptions.value = options || []
+}
+
+const parseTemplateFields = (source: unknown): TemplateField[] => {
+  let parsed = source
+
+  for (let index = 0; index < 2 && typeof parsed === 'string'; index += 1) {
+    if (!parsed.trim()) return []
+    try {
+      parsed = JSON.parse(parsed)
+    } catch {
+      return []
+    }
+  }
+
+  if (Array.isArray(parsed)) return parsed
+  if (!parsed || typeof parsed !== 'object') return []
+
+  const container = parsed as Record<string, unknown>
+  const fields = container.fields || container.list || container.data
+  return Array.isArray(fields) ? fields : []
+}
+
+const loadTemplateFields = async (detail: DeviceDetail) => {
+  const savedFields = parseTemplateFields(detail.templateJson)
+  if (!detail.assetClass) {
+    templateFields.value = savedFields
+    return
+  }
 
 
   try {
   try {
-    const fields = JSON.parse(formData.value.templateJson)
-    return Array.isArray(fields) ? fields : []
+    const definitions = (await DeviceAttrModelApi.getDeviceAttrModelListByDeviceCategoryId(
+      detail.assetClass
+    )) as DeviceAttrModelData[]
+    const savedFieldMap = new Map(
+      savedFields.filter((field) => field.code).map((field) => [field.code, field])
+    )
+
+    const fieldsFromDefinitions: TemplateField[] = (definitions || []).map((definition) => {
+      const code = definition.code || ''
+      const savedField = savedFieldMap.get(code)
+      savedFieldMap.delete(code)
+      return {
+        ...definition,
+        ...savedField,
+        value: savedField?.value ?? detail[code] ?? definition.defaultValue ?? null
+      } as TemplateField
+    })
+
+    templateFields.value = [...fieldsFromDefinitions, ...savedFieldMap.values()]
   } catch {
   } catch {
-    return []
+    templateFields.value = savedFields
   }
   }
-})
+}
 
 
 const mainInfoFields = computed<MainInfoField[]>(() => [
 const mainInfoFields = computed<MainInfoField[]>(() => [
+  {
+    prop: 'yfClass',
+    label: t('iotDevice.yfClass'),
+    value: yfClassLabel.value,
+    icon: 'ep:connection'
+  },
   {
   {
     prop: 'yfDeviceCode',
     prop: 'yfDeviceCode',
     label: t('iotDevice.yfCode'),
     label: t('iotDevice.yfCode'),
@@ -267,7 +358,7 @@ const mainInfoFields = computed<MainInfoField[]>(() => [
   },
   },
   {
   {
     prop: 'deviceStatus',
     prop: 'deviceStatus',
-    label: '施工状态',
+    label: t('iotDevice.status'),
     value: statusLabel.value,
     value: statusLabel.value,
     icon: 'ep:operation'
     icon: 'ep:operation'
   },
   },
@@ -283,11 +374,48 @@ const mainInfoFields = computed<MainInfoField[]>(() => [
     value: formatValue(formData.value.model),
     value: formatValue(formData.value.model),
     icon: 'ep:set-up'
     icon: 'ep:set-up'
   },
   },
+  {
+    prop: 'carNo',
+    label: '车牌号',
+    value: formatValue(formData.value.carNo),
+    icon: 'ep:van'
+  },
+  {
+    prop: 'deviceNo',
+    label: '设备号',
+    value: formatValue(formData.value.deviceNo),
+    icon: 'ep:document'
+  },
+  {
+    prop: 'useProject',
+    label: t('deviceForm.useProject'),
+    value: formatValue(formData.value.useProject),
+    icon: 'ep:briefcase'
+  },
+  {
+    prop: 'assetOwnership',
+    label: t('deviceForm.assetOwner'),
+    value: formatValue(formData.value.assetOwnership),
+    icon: 'ep:stamp'
+  },
+  {
+    prop: 'address',
+    label: '所在地点',
+    value: formatValue(formData.value.address),
+    icon: 'ep:location'
+  },
+
   {
   {
     prop: 'responsibleNames',
     prop: 'responsibleNames',
     label: t('devicePerson.rp'),
     label: t('devicePerson.rp'),
     value: responsibleNames.value,
     value: responsibleNames.value,
     icon: 'ep:user'
     icon: 'ep:user'
+  },
+  {
+    prop: 'remark',
+    label: t('deviceForm.remark'),
+    value: formatValue(formData.value.remark),
+    icon: 'ep:chat-line-square'
   }
   }
 ])
 ])
 
 
@@ -299,6 +427,7 @@ const getDetail = async () => {
     const detail = (await IotDeviceApi.getIotDevice(deviceId.value)) as DeviceDetail
     const detail = (await IotDeviceApi.getIotDevice(deviceId.value)) as DeviceDetail
     formData.value = detail || {}
     formData.value = detail || {}
     defaultPicUrl.value = detail?.picUrl || defaultDevicePic
     defaultPicUrl.value = detail?.picUrl || defaultDevicePic
+    await loadTemplateFields(formData.value)
   } finally {
   } finally {
     formLoading.value = false
     formLoading.value = false
   }
   }
@@ -318,6 +447,7 @@ const imagePreview = (imgUrl: string) => {
 
 
 onMounted(() => {
 onMounted(() => {
   getDetail()
   getDetail()
+  getEncodingTypeOptions()
 })
 })
 </script>
 </script>
 
 
@@ -339,7 +469,7 @@ onMounted(() => {
 
 
 .device-main {
 .device-main {
   display: grid;
   display: grid;
-  grid-template-columns: 292px minmax(0, 1fr);
+  grid-template-columns: 264px minmax(0, 1fr);
   min-height: 286px;
   min-height: 286px;
   padding: 24px;
   padding: 24px;
   background: radial-gradient(circle at 18px 22px, rgb(44 123 229 / 12%), transparent 30%),
   background: radial-gradient(circle at 18px 22px, rgb(44 123 229 / 12%), transparent 30%),
@@ -348,19 +478,19 @@ onMounted(() => {
 
 
 .device-media {
 .device-media {
   display: flex;
   display: flex;
-  align-items: stretch;
+  align-items: center;
   justify-content: center;
   justify-content: center;
 }
 }
 
 
 .device-image-frame {
 .device-image-frame {
   position: relative;
   position: relative;
-  width: 260px;
-  height: 226px;
-  padding: 8px;
+  width: 236px;
+  height: 236px;
+  padding: 12px;
   overflow: hidden;
   overflow: hidden;
   font: inherit;
   font: inherit;
   cursor: zoom-in;
   cursor: zoom-in;
-  background: var(--el-bg-color);
+  background: linear-gradient(145deg, #fff 0%, #f7faff 100%);
   border: 1px solid rgb(44 123 229 / 14%);
   border: 1px solid rgb(44 123 229 / 14%);
   border-radius: 12px;
   border-radius: 12px;
   box-shadow: 0 14px 32px rgb(39 83 135 / 15%);
   box-shadow: 0 14px 32px rgb(39 83 135 / 15%);
@@ -381,6 +511,7 @@ onMounted(() => {
   width: 100%;
   width: 100%;
   height: 100%;
   height: 100%;
   overflow: hidden;
   overflow: hidden;
+  background: transparent;
   border-radius: 8px;
   border-radius: 8px;
   transition: transform 0.3s ease;
   transition: transform 0.3s ease;
 }
 }
@@ -472,8 +603,8 @@ onMounted(() => {
 
 
 .device-info-grid {
 .device-info-grid {
   display: grid;
   display: grid;
-  grid-template-columns: repeat(5, minmax(132px, 1fr));
-  gap: 0 22px;
+  grid-template-columns: repeat(6, minmax(120px, 1fr));
+  gap: 0 18px;
   padding-top: 14px;
   padding-top: 14px;
 }
 }
 
 
@@ -540,11 +671,11 @@ onMounted(() => {
 
 
 @media (width <= 1399px) {
 @media (width <= 1399px) {
   .device-main {
   .device-main {
-    grid-template-columns: 260px minmax(0, 1fr);
+    grid-template-columns: 232px minmax(0, 1fr);
   }
   }
 
 
   .device-image-frame {
   .device-image-frame {
-    width: 232px;
+    width: 208px;
     height: 208px;
     height: 208px;
   }
   }