diff --git a/package.json b/package.json index 92a666e..872d270 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "vue": "^3.2.40", "vue-echarts": "^6.2.3", "vue-i18n": "^9.2.2", + "vue-json-pretty": "^2.4.0", "vue-quill-editor": "^3.0.6", "vue-router": "^4.0.14", "xlsx": "^0.18.5" diff --git a/src/api/device.ts b/src/api/device.ts index c699379..9139783 100644 --- a/src/api/device.ts +++ b/src/api/device.ts @@ -11,11 +11,13 @@ export interface DeviceRecord { pageable?: string; longitude?: number; latitude?: number; - icon?: string; + icon?: object; hardwareVersion?: string; firmwareVersion?: string; productName?: string; id?: number; + warning?: string; + iconId?: number; } export interface DeviceCreateRecord { diff --git a/src/api/tsl.ts b/src/api/tsl.ts index c655bf7..7e5e29a 100644 --- a/src/api/tsl.ts +++ b/src/api/tsl.ts @@ -52,12 +52,8 @@ export function queryEventList(data: eventRecord) { } // 事件记录 -export function queryEventLog(data: eventRecord) { - return axios({ - url: '/api/rest/tsl/event/log', - method: 'get', - params: data, - }); +export function queryEventLog(clientId: number) { + return axios.get(`/api/rest/tsl/event/${clientId}/log`) } export function createServe(data: any) { diff --git a/src/views/iot/device/components/device-detail.vue b/src/views/iot/device/components/device-detail.vue index 56d02e5..385762c 100644 --- a/src/views/iot/device/components/device-detail.vue +++ b/src/views/iot/device/components/device-detail.vue @@ -2,9 +2,11 @@ <div class="container"> <Breadcrumb :items="['物联网管理', '设备管理', '设备详情']" /> <a-card class="general-card" title=" "> - <a-descriptions v-model="renderData" size="large"> - <template #title - ><h3 style="margin-top: -15px" + <a-row :gutter="16"> + <a-col :span="16"> + <a-descriptions v-model="renderData" size="large"> + <template #title + ><h3 style="margin-top: -15px" ><a-button class="nav-btn" type="outline" @@ -13,26 +15,31 @@ > <icon-undo /> </a-button> - 设备详情</h3 - > - </template> - <a-descriptions-item label="设备名称">{{ - renderData.name - }}</a-descriptions-item> - <a-descriptions-item label="硬件版本">{{ - renderData.hardwareVersion - }}</a-descriptions-item> - <a-descriptions-item label="固件版本">{{ - renderData.firmwareVersion - }}</a-descriptions-item> - <a-descriptions-item label="所属产品">{{ - renderData.productName - }}</a-descriptions-item> - <!-- <a-descriptions-item label="备注">{{renderData.remark }}</a-descriptions-item>--> - <a-descriptions-item label="创建时间">{{ - dayjs(renderData.createTime).format('YYYY-MM-DD HH:mm:ss') - }}</a-descriptions-item> - </a-descriptions> + 设备详情</h3 + > + </template> + <a-descriptions-item label="设备名称">{{ + renderData.name + }}</a-descriptions-item> + <a-descriptions-item label="硬件版本">{{ + renderData.hardwareVersion + }}</a-descriptions-item> + <a-descriptions-item label="固件版本">{{ + renderData.firmwareVersion + }}</a-descriptions-item> + <a-descriptions-item label="所属产品">{{ + renderData.productName + }}</a-descriptions-item> + <!-- <a-descriptions-item label="备注">{{renderData.remark }}</a-descriptions-item>--> + <a-descriptions-item label="创建时间">{{ + dayjs(renderData.createTime).format('YYYY-MM-DD HH:mm:ss') + }}</a-descriptions-item> + </a-descriptions> + </a-col> + <a-col :span="4"> + <a-image :src="previewUrl" width="100px" /> + </a-col> + </a-row> </a-card> <a-card class="general-card" style="margin-top: 10px"> <a-tabs @@ -89,14 +96,8 @@ <div>{{ dayjs(record.recordTime).format('YYYY-MM-DD HH:mm:ss') }}</div> </template> <template #content="{ record }"> - <div v-if="record.content"> - <span v-for="(value, key, index) in record.content" :key="key"> - <template v-if="index === 0"> - <strong>{{ key }}:</strong> {{ value }} - </template> - </span> - <span v-if="Object.keys(record.content).length > 1">. . . . . .</span> - </div> + <vue-json-pretty v-if="record.content" :data="record.content" :deep="0" /> + <div v-else>/</div> </template> <template #operations="{ record }"> <a-button type="text" @click="openDetailModal(record)"> @@ -108,8 +109,16 @@ </a-tab-pane> <a-tab-pane key="5" tab="事件记录" title="事件记录"> <a-table :columns="deviceEventColumns" :data="deviceEventData" > + <template #recordTime="{ record }"> + <div>{{ dayjs(record.recordData.recordTime).format('YYYY-MM-DD HH:mm:ss') }}</div> + </template> + <template #content="{ record }"> + <vue-json-pretty v-if="record.recordData.content" :data="record.recordData.content" :deep="0" > + </vue-json-pretty> + <div v-else>/</div> + </template> <template #operations="{ record }"> - <a-button type="text" @click="openDetailModal(record)"> + <a-button type="text" @click="openEventDetailModal(record)"> <icon-list /> 详情 </a-button> @@ -150,11 +159,33 @@ <a-button @click="closeDetailModal">关闭</a-button> </template> <div v-if="selectedRecord"> - <p><strong>上报时间:</strong> {{ dayjs(selectedRecord.recordTime).format('YYYY-MM-DD HH:mm:ss') }}</p> - <div v-if="selectedRecord.content"> - <div v-for="(value, key) in selectedRecord.content" :key="key"> - <strong>{{ key }}:</strong> {{ value }} - </div> + <p> + <strong>上报时间:</strong> {{ dayjs(selectedRecord.recordTime).format('YYYY-MM-DD HH:mm:ss') }} + </p> + <strong>内容:</strong> + <div style="border: 1px solid #e5e5e5; border-radius: 4px; padding: 8px; margin-top: 8px;"> + <vue-json-pretty :data="selectedRecord.content" :deep="1" /> + </div> + </div> + </a-modal> + + <!-- 事件记录详情模态框 --> + <a-modal + :visible="eventDetailVisible" + @cancel="closeEventDetailModal" + title="事件记录详情" + > + <template #footer> + <a-button @click="closeEventDetailModal">关闭</a-button> + </template> + <div v-if="selectedEventRecord"> + <strong>事件名称:</strong> {{ selectedEventRecord.name }} + <p> + <strong>发生时间:</strong> {{ dayjs(selectedEventRecord.recordData.recordTime).format('YYYY-MM-DD HH:mm:ss') }} + </p> + <strong>内容:</strong> + <div style="border: 1px solid #e5e5e5; border-radius: 4px; padding: 8px; margin-top: 8px;"> + <vue-json-pretty :data="selectedEventRecord.recordData.content" :deep="1" /> </div> </div> </a-modal> @@ -164,13 +195,15 @@ <script lang="ts" setup> import dayjs from 'dayjs'; import { useRoute } from 'vue-router'; - import { onMounted, ref } from 'vue'; + import { onMounted, reactive, ref } from 'vue'; import { Message } from '@arco-design/web-vue'; import { queryDeviceDetail, queryDeviceRecord, sendCommand } from '@/api/device'; import { queryEventLog, queryServeDetail, queryServeList } from '@/api/tsl'; import useVisible from '@/hooks/visible'; + import VueJsonPretty from 'vue-json-pretty'; import dynamicForm from './dynamic-form.vue'; import DeviceMap from './device-map.vue'; + import 'vue-json-pretty/lib/styles.css'; const route = useRoute(); const { visible, setVisible } = useVisible(); @@ -207,6 +240,7 @@ title: '内容', dataIndex: 'content', slotName: 'content', + ellipsis: true, }, { title: '操作', @@ -237,6 +271,16 @@ dataIndex: 'name', slotName: 'name', }, + { + title: '发生时间', + dataIndex: 'recordData.recordTime', + slotName: 'recordTime', + }, + { + title: '内容', + dataIndex: 'recordData.content', + slotName: 'content', + }, { title: '操作', dataIndex: 'operations', @@ -244,10 +288,15 @@ }, ];; const activeKey = ref('1'); + const previewUrl = ref(''); const detailVisible = ref(false); const selectedRecord = ref<any>(null); + // 事件记录详情模态框状态 + const eventDetailVisible = ref(false); + const selectedEventRecord = ref<any>(null); + const openDetailModal = (record: any) => { selectedRecord.value = record; detailVisible.value = true; @@ -262,6 +311,9 @@ { clientId: 0, productId: 0, + preview: { + url: '', + }, } ); const deviceReportData = ref([]); @@ -274,6 +326,7 @@ // 1. 先获取设备详情 const detailRes = await queryDeviceDetail(Id); renderData.value = detailRes.data; + previewUrl.value = renderData.value.preview.url; // 2. 根据设备详情中的 clientId 和 productId 调用其他接口 const [recordRes, serveRes, eventRes] = await Promise.all([ @@ -287,11 +340,7 @@ size: 10, current: 1, }), - queryEventLog({ - clientId: renderData.value.clientId, - size: 10, - current: 1, - }), + queryEventLog(renderData.value.clientId), ]); // 3. 更新其他数据 @@ -355,6 +404,18 @@ } }; + // 打开事件记录详情模态框 + const openEventDetailModal = (record: any) => { + selectedEventRecord.value = record; + eventDetailVisible.value = true; + }; + + // 关闭事件记录详情模态框 + const closeEventDetailModal = () => { + eventDetailVisible.value = false; + selectedEventRecord.value = null; + }; + onMounted(() => { fetchData(id); }); diff --git a/src/views/iot/device/components/device-edit.vue b/src/views/iot/device/components/device-edit.vue index 17a5284..6ee4ac1 100644 --- a/src/views/iot/device/components/device-edit.vue +++ b/src/views/iot/device/components/device-edit.vue @@ -61,6 +61,21 @@ > <a-input v-model="formData.name" placeholder="请输入设备名称" /> </a-form-item> + <!-- 客户机序列号 --> + <a-form-item + field="clientId" + label="客户机ID" + :rules="[ + { max: 12, message: '客户机ID不能超过12位' } + ]" + :validate-trigger="['change']" + > + <a-input + v-model="formData.clientId" + placeholder="请输入客户机ID" + :max-length="12" + /> + </a-form-item> <!-- 硬件版本 --> <a-form-item field="hardwareVersion" @@ -85,6 +100,29 @@ placeholder="请输入固件版本" /> </a-form-item> + <!-- 纬度 --> + <a-form-item + field="latitude" + label="纬度" + :validate-trigger="['change']" + > + <a-input + v-model="formData.latitude" + placeholder="请输入纬度" + /> + </a-form-item> + <!-- 经度 --> + <a-form-item + field="longitude" + label="经度" + :validate-trigger="['change']" + > + <a-input + v-model="formData.longitude" + placeholder="请输入经度" + /> + </a-form-item> + <a-form-item field="remark" label="备注"> <a-textarea v-model="formData.remark" placeholder="请输入描述" /> </a-form-item> @@ -157,7 +195,6 @@ import '@wangeditor/editor/dist/css/style.css'; import { createDevice, - queryDeviceByName, queryDeviceDetail, queryProductByName, updateDevice } from '@/api/device'; diff --git a/src/views/iot/device/index.vue b/src/views/iot/device/index.vue index 508232f..a8230fb 100644 --- a/src/views/iot/device/index.vue +++ b/src/views/iot/device/index.vue @@ -47,7 +47,7 @@ </a-form-item> </a-col> <a-col :span="10"> - <a-form-item field="isOnline" label="在线"> + <a-form-item field="online" label="在线"> <a-select v-model="formModel.isOnline" style="width: 360px" diff --git a/src/views/iot/deviceMap/index.vue b/src/views/iot/deviceMap/index.vue index c5008ef..0eb532a 100644 --- a/src/views/iot/deviceMap/index.vue +++ b/src/views/iot/deviceMap/index.vue @@ -1,6 +1,6 @@ <template> <a-layout> - <a-layout-sider style="width:300px;" :resize-directions="['right']"> + <a-layout-sider style="width:300px;background-color: transparent;" :resize-directions="['right']"> <!-- 搜索框 --> <a-card> <a-input-search @@ -35,8 +35,7 @@ :size="70" > <a-image - :src=device.previewUrl - @error="handleImageError" + :src="getImageUrl(device.previewId)" /> </a-avatar> <a-descriptions layout="inline-horizontal" :style="{ marginLeft: '10px'}"> @@ -50,23 +49,6 @@ </a-card> </a-col> </a-row> -<!-- <a-list>--> -<!-- <a-list-item v-for="device in deviceList" :key="device.id" @click="selectDevice(device)">--> -<!-- <a-list-item-meta--> -<!-- :title="device.name"--> -<!-- :description="device.name"--> -<!-- >--> -<!-- <template #avatar>--> -<!-- <a-avatar shape="square">--> -<!-- <img--> -<!-- alt="avatar"--> -<!-- src="https://p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/3ee5f13fb09879ecb5185e440cef6eb9.png~tplv-uwbnlip3yd-webp.webp"--> -<!-- />--> -<!-- </a-avatar>--> -<!-- </template>--> -<!-- </a-list-item-meta>--> -<!-- </a-list-item>--> -<!-- </a-list>--> </a-layout-sider> <a-layout-content> <div id="container"></div> @@ -92,6 +74,15 @@ }; + // 获取图片 URL 的函数 + const getImageUrl = (previewId: string) => { + if (!previewId) { + return 'https://p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/3ee5f13fb09879ecb5185e440cef6eb9.png~tplv-uwbnlip3yd-webp.webp'; + } + return `/api/rest/attachment/${previewId}`; + }; + + // 防抖搜索函数 const handleSearch = debounce(async (value: string) => { try { @@ -113,10 +104,10 @@ } }, 300); // 防抖时间设置为 300ms - const handleImageError = (event: Event) => { - const img = event.target as HTMLImageElement; - img.src = 'https://p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/3ee5f13fb09879ecb5185e440cef6eb9.png~tplv-uwbnlip3yd-webp.webp'; // 替换为默认图片 - }; + // const handleImageError = (event: Event) => { + // const img = event.target as HTMLImageElement; + // img.src = 'https://p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/3ee5f13fb09879ecb5185e440cef6eb9.png~tplv-uwbnlip3yd-webp.webp'; // 替换为默认图片 + // }; AMapLoader.load({ key: 'a4e80eed798a56451b226dcfca81b846', // 申请好的Web端开发者Key,首次调用 load 时必填 @@ -136,9 +127,11 @@ const lngLat = [device.longitude, device.latitude]; const content = document.createElement('div'); content.className = 'custom-content-marker'; - content.innerHTML = ` - <img src="https://webapi.amap.com/theme/v1.3/markers/n/mark_b.png" alt="标记点" />`; + const markerIcon = device.warning === '1' + ? `/api/rest/attachment/${device.icon?.url}` // 警告标记 + : `/api/rest/attachment/${device.icon?.url}`; // 默认标记 + content.innerHTML = `<img src="${markerIcon}" alt="标记点" />`; // 创建标记点 const marker = new AMap.Marker({ position: lngLat, diff --git a/src/views/iot/product/components/product-detail.vue b/src/views/iot/product/components/product-detail.vue index 6ad1b1b..5342b4c 100644 --- a/src/views/iot/product/components/product-detail.vue +++ b/src/views/iot/product/components/product-detail.vue @@ -2,39 +2,45 @@ <div class="container"> <Breadcrumb :items="['物联网管理', '产品管理', '产品详情']" /> <a-card class="general-card" title=" "> - <a-descriptions v-model="renderData" size="large"> - <template #title - ><h3 style="margin-top: -15px"> - <a-button - class="nav-btn" - type="outline" - :shape="'circle'" - @click="() => $router.go(-1)" - > - <icon-undo /> - </a-button> - 产品详情</h3 - > - </template> - <a-descriptions-item label="产品名称">{{ - renderData.name - }}</a-descriptions-item> - <a-descriptions-item label="产品类型">{{ - renderData.productType - }}</a-descriptions-item> - <a-descriptions-item label="产品型号">{{ - renderData.model - }}</a-descriptions-item> - <a-descriptions-item label="通讯协议">{{ - renderData.link - }}</a-descriptions-item> - <a-descriptions-item label="备注">{{ - renderData.remark - }}</a-descriptions-item> - <a-descriptions-item label="创建时间">{{ - dayjs(renderData.createTime).format('YYYY-MM-DD HH:mm:ss') - }}</a-descriptions-item> - </a-descriptions> + <a-row :gutter="10"> + <a-col :span="16"> + <a-descriptions v-model="renderData" size="large"> + <template #title + ><h3 style="margin-top: -15px"> + <a-button + class="nav-btn" + type="outline" + :shape="'circle'" + @click="() => $router.go(-1)" + > + <icon-undo /> + </a-button> + 产品详情</h3> + </template> + <a-descriptions-item label="产品名称">{{ + renderData.name + }}</a-descriptions-item> + <a-descriptions-item label="产品类型">{{ + renderData.productType + }}</a-descriptions-item> + <a-descriptions-item label="产品型号">{{ + renderData.model + }}</a-descriptions-item> + <a-descriptions-item label="通讯协议">{{ + renderData.link + }}</a-descriptions-item> + <a-descriptions-item label="备注">{{ + renderData.remark + }}</a-descriptions-item> + <a-descriptions-item label="创建时间">{{ + dayjs(renderData.createTime).format('YYYY-MM-DD HH:mm:ss') + }}</a-descriptions-item> + </a-descriptions> + </a-col> + <a-col :span="4"> + <a-image :src="previewUrl" width="100px" /> + </a-col> + </a-row> </a-card> <a-card class="general-card" style="margin-top: 10px"> <a-tabs @@ -88,9 +94,11 @@ ]; const activeKey = ref('1'); const renderData = ref([]); + const previewUrl = ref(''); const fetchData = async (Id: number) => { const res = await queryProductDetail(Id); renderData.value = res.data; + previewUrl.value = res.data.preview.url; }; const handleMenuClick = (e: any) => { activeKey.value = e; diff --git a/src/views/iot/product/components/product-edit.vue b/src/views/iot/product/components/product-edit.vue index b8e937c..48304d2 100644 --- a/src/views/iot/product/components/product-edit.vue +++ b/src/views/iot/product/components/product-edit.vue @@ -84,6 +84,7 @@ <a-upload show-file-list :file-list="previewList" + image-preview :limit="1" list-type="picture-card" :custom-request="(option: any) => customRequest(option, 'preview')" @@ -96,6 +97,7 @@ > <a-upload show-file-list + list-type="picture" :file-list="iconList" :limit="1" :custom-request="(option: any) => customRequest(option, 'icon')" @@ -275,11 +277,11 @@ onSuccess(res.data); if (type === 'preview') { formData.value.preview = formData.value.preview || []; - formData.value.preview.push(res.data); + formData.value.preview = res.data; formData.value.previewId = res.data.id; } else if (type === 'icon') { formData.value.icon = formData.value.icon || []; - formData.value.icon.push(res.data); + formData.value.icon = res.data; formData.value.iconId = res.data.id; } } else { @@ -295,7 +297,7 @@ } // 调用删除接口 const res = await deleteAttachment(fileId); - if (res.status === 200) { + if (res.data === true) { // 删除成功后 Message.success('删除成功'); } else {