feat: 历史记录页面优化及数据分析卡片改造-物联设备点位页面优化

main
zhongwenkai 2 weeks ago
parent 9936bdb0d3
commit 8a2e5d9104

@ -17,6 +17,7 @@
:rules="activeRules" :rules="activeRules"
label-width="120px" label-width="120px"
v-loading="formLoading" v-loading="formLoading"
class="pb-20px"
> >
<template v-if="formType === 'create' || formType === 'update'"> <template v-if="formType === 'create' || formType === 'update'">
<el-form-item :label="t('DataCollection.Device.deviceCode')" prop="deviceCode"> <el-form-item :label="t('DataCollection.Device.deviceCode')" prop="deviceCode">

@ -1,4 +1,5 @@
<template> <template>
<div v-show="!showPointPage">
<ContentWrap> <ContentWrap>
<div class="device-statistics"> <div class="device-statistics">
<div <div
@ -249,7 +250,7 @@
<div class="content"> <div class="content">
<div class="info-row"> <div class="info-row">
<div class="info-label"> <div class="info-label">
<i>📡</i> <Icon icon="ep:connection" />
采集协议 采集协议
</div> </div>
<div class="info-value"> <div class="info-value">
@ -258,7 +259,7 @@
</div> </div>
<div class="info-row"> <div class="info-row">
<div class="info-label"> <div class="info-label">
<i></i> <Icon icon="ep:timer" />
采集时间 采集时间
</div> </div>
<div class="info-value"> <div class="info-value">
@ -268,7 +269,7 @@
<div class="toggle-container" @click.stop> <div class="toggle-container" @click.stop>
<div class="toggle-label"> <div class="toggle-label">
<i>🔘</i> <Icon icon="ep:switch" />
是否启用 是否启用
</div> </div>
<el-switch <el-switch
@ -286,11 +287,11 @@
<div class="footer" @click.stop> <div class="footer" @click.stop>
<div class="action-icons"> <div class="action-icons">
<el-button class="action-btn btn-primary" @click.stop="openDetailForm(item?.id,item?.deviceName)" > <el-button class="action-btn btn-primary" @click.stop="openDetailForm(item?.id,item?.deviceName)" >
<i>📍</i> <Icon icon="ep:location" />
{{ t('DataCollection.Device.attributeModuleName') }} {{ t('DataCollection.Device.attributeModuleName') }}
</el-button> </el-button>
<el-button class="action-btn btn-success" @click.stop="openForm('setting', item?.id)" v-hasPermi="['iot:device:update']"> <el-button class="action-btn btn-success" @click.stop="openForm('setting', item?.id)" v-hasPermi="['iot:device:update']">
<i></i> <Icon icon="ep:setting" />
{{ t('DataCollection.Device.settingDialogTitle') }} {{ t('DataCollection.Device.settingDialogTitle') }}
</el-button> </el-button>
@ -306,7 +307,7 @@
@click="handleCopy(item?.id)" @click="handleCopy(item?.id)"
v-hasPermi="['iot:device:create']" v-hasPermi="['iot:device:create']"
> >
📋 <Icon icon="ep:document-copy" />
</el-button> </el-button>
</el-tooltip> </el-tooltip>
<el-tooltip <el-tooltip
@ -320,7 +321,7 @@
@click.stop="handleEdit(item)" @click.stop="handleEdit(item)"
v-hasPermi="['iot:device:update']" v-hasPermi="['iot:device:update']"
> >
<Icon icon="ep:edit" />
</el-button> </el-button>
</el-tooltip> </el-tooltip>
<el-tooltip <el-tooltip
@ -328,7 +329,7 @@
placement="top" placement="top"
effect="dark" effect="dark"
> >
<el-button class="icon-btn" @click.stop="changeTable">🏠</el-button> <el-button class="icon-btn" @click.stop="changeTable"><Icon icon="ep:list" /></el-button>
</el-tooltip> </el-tooltip>
</div> </div>
</div> </div>
@ -342,13 +343,19 @@
</ContentWrap> </ContentWrap>
</div>
<!-- 表单弹窗添加/修改 --> <!-- 表单弹窗添加/修改 -->
<DeviceForm ref="formRef" @success="getListOne" /> <DeviceForm ref="formRef" @success="getListOne" />
<!-- 子表的列表 --> <!-- 子表的列表点位管理整页 -->
<!-- <ContentWrap v-if="ifShow"> <ContentWrap v-show="showPointPage">
<template v-if="attributeDeviceId"> <template v-if="attributeDeviceId">
<div class="mb-10px flex items-center justify-between text-sm text-gray-500"> <div class="mb-10px flex items-center justify-between text-sm text-gray-500">
<div>
<el-button type="primary" link @click="closePointPage">
<Icon icon="ep:arrow-left" class="mr-5px" /> 返回设备列表
</el-button>
</div>
<div> <div>
{{ t('DataCollection.Device.currentDeviceLabel') }}<span class="font-medium text-gray-700">{{ attributeDeviceName || '-' }}</span> {{ t('DataCollection.Device.currentDeviceLabel') }}<span class="font-medium text-gray-700">{{ attributeDeviceName || '-' }}</span>
</div> </div>
@ -735,7 +742,7 @@
</el-tabs> </el-tabs>
</template> </template>
<el-empty v-else :description="t('DataCollection.Device.emptyDescription')" /> <el-empty v-else :description="t('DataCollection.Device.emptyDescription')" />
</ContentWrap>--> </ContentWrap>
<Dialog :title="t('DataCollection.Device.alarmHistoryTitle')" v-model="deviceAlarmDialogVisible" width="1200px"> <Dialog :title="t('DataCollection.Device.alarmHistoryTitle')" v-model="deviceAlarmDialogVisible" width="1200px">
<el-form <el-form
@ -835,9 +842,6 @@
/> />
</div> </div>
</Dialog> </Dialog>
<!-- 表单弹窗弹出设备属性 -->
<DetailForm ref="detailFormRef" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -856,10 +860,10 @@ import {
Location Location
} from '@element-plus/icons-vue' } from '@element-plus/icons-vue'
import {useRouter} from "vue-router"; import {useRouter} from "vue-router";
import DetailForm from "@/views/iot/device/detailForm.vue";
import {ProductUnitApi, ProductUnitVO} from "@/api/erp/product/unit"; const ifShow =ref(false)
const ifShow =ref(true) const currentView = ref('grid') // 'table' 'grid'
const currentView = ref('table') // 'table' 'grid' const showPointPage = ref(false) //
// script setup // script setup
const noImageUrl = const noImageUrl =
'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODAiIGhlaWdodD0iODAiIHZpZXdCb3g9IjAgMCA4MCA4MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iODAiIGhlaWdodD0iODAiIGZpbGw9IiNFOUVDRUYiLz48dGV4dCB4PSI0MCIgeT0iNDUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM5OTkiIGZvbnQtc2l6ZT0iMTQiPuWbvueJh+WKoOi9veWksei0pTwvdGV4dD48L3N2Zz4=' 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODAiIGhlaWdodD0iODAiIHZpZXdCb3g9IjAgMCA4MCA4MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iODAiIGhlaWdodD0iODAiIGZpbGw9IiNFOUVDRUYiLz48dGV4dCB4PSI0MCIgeT0iNDUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM5OTkiIGZvbnQtc2l6ZT0iMTQiPuWbvueJh+WKoOi9veWksei0pTwvdGV4dD48L3N2Zz4='
@ -1134,13 +1138,18 @@ const formRef = ref()
const openForm = (type: string, id?: number) => { const openForm = (type: string, id?: number) => {
formRef.value.open(type, id) formRef.value.open(type, id)
} }
/** 设备属性弹出框 */ /** 设备属性弹出框 - 改为整页内嵌显示 */
const detailFormRef = ref() const openDetailForm = (id?: number, deviceName?: string) => {
const openDetailForm = (id?: number,deviceName?:string) => { if (!id) return
attributeDeviceId.value = id
attributeDeviceName.value = deviceName || ''
deviceTabActive.value = 'deviceAttribute'
showPointPage.value = true
}
const closePointPage = () => {
showPointPage.value = false
attributeDeviceId.value = undefined attributeDeviceId.value = undefined
attributeDeviceName.value = '' attributeDeviceName.value = ''
detailFormRef.value.open(id,deviceName)
//formRef.value.open(id,deviceName)
} }
const enabledChange = (row: DeviceVO, value: boolean) => { const enabledChange = (row: DeviceVO, value: boolean) => {
@ -1252,16 +1261,7 @@ const deviceRuleTabLabel = computed(() => {
}) })
const handleShowAttribute = (row: any) => { const handleShowAttribute = (row: any) => {
attributeDeviceId.value = row?.id openDetailForm(row?.id, row?.deviceName)
attributeDeviceName.value = row?.deviceName ?? ''
router.push({
path: '/iot/pointManagement',
query: {
id: row?.id,
name: row?.deviceName
}
});
} }
const ruleLoading = ref(false) const ruleLoading = ref(false)

@ -125,33 +125,24 @@
</div> </div>
<div class="point-list"> <div class="point-list">
<el-collapse v-model="activePointGroup"> <div v-for="group in filteredPointGroups" :key="group.group" class="point-group-card">
<el-collapse-item <div class="point-group-header">{{ group.group }} ({{ group.points.length }})</div>
v-for="group in filteredPointGroups" <div class="point-card-grid">
:key="group.group" <div
:title="`${group.group} (${group.points.length})`" v-for="point in group.points"
:name="group.group" :key="point.code"
> :class="['point-card', { 'point-card--selected': selectedPointCodes.includes(point.code) }]"
<el-checkbox-group v-model="selectedPointCodes"> @click="togglePoint(point.code)"
<el-space wrap> >
<el-checkbox <div class="point-card-name">{{ point.name }}</div>
v-for="point in group.points" <div class="point-card-code">{{ point.code }}</div>
:key="point.code" <div class="point-card-meta">
:label="point.code" <span class="point-card-unit">{{ point.unit }}</span>
> <span class="point-card-range">{{ point.lower }} ~ {{ point.upper }}</span>
<div class="point-info"> </div>
<div class="point-name">{{ point.name }}</div> </div>
<div class="point-detail"> </div>
<span class="point-code">{{ point.code }}</span> </div>
<span class="point-unit">{{ point.unit }}</span>
<span class="point-range">({{ point.lower }} ~ {{ point.upper }})</span>
</div>
</div>
</el-checkbox>
</el-space>
</el-checkbox-group>
</el-collapse-item>
</el-collapse>
</div> </div>
</div> </div>
</el-form-item> </el-form-item>
@ -465,6 +456,15 @@ const pointKeyword = ref('')
const activePointGroup = ref([]) const activePointGroup = ref([])
const selectedPointCodes = ref([]) const selectedPointCodes = ref([])
const togglePoint = (code: string) => {
const idx = selectedPointCodes.value.indexOf(code)
if (idx > -1) {
selectedPointCodes.value.splice(idx, 1)
} else {
selectedPointCodes.value.push(code)
}
}
// //
const selectedPoints = computed(() => { const selectedPoints = computed(() => {
const allPoints = pointGroups.value.flatMap(g => g.points) const allPoints = pointGroups.value.flatMap(g => g.points)
@ -1177,9 +1177,87 @@ window.addEventListener('resize', () => {
} }
.point-list { .point-list {
max-height: 230px; max-height: 320px;
overflow: auto; overflow: auto;
padding: 12px; padding: 12px;
.point-group-card {
margin-bottom: 12px;
&:last-child {
margin-bottom: 0;
}
}
.point-group-header {
margin-bottom: 8px;
padding: 4px 0;
font-size: 13px;
font-weight: 600;
color: #344054;
}
.point-card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 8px;
}
.point-card {
padding: 10px 12px;
background: #fff;
border: 1px solid var(--el-border-color);
border-radius: 6px;
cursor: pointer;
transition: all 0.15s ease;
&:hover {
border-color: var(--el-color-primary-light-3);
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.06);
}
&--selected {
border-color: var(--el-color-primary);
background: var(--el-color-primary-light-9);
box-shadow: 0 2px 8px rgba(var(--el-color-primary-rgb, 64, 158, 255), 0.12);
}
.point-card-name {
font-size: 14px;
font-weight: 600;
color: #1d2939;
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.point-card-code {
font-size: 11px;
color: var(--el-text-color-placeholder);
margin-bottom: 6px;
word-break: break-all;
overflow-wrap: break-word;
line-height: 1.4;
}
.point-card-meta {
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
font-size: 11px;
.point-card-unit {
color: var(--el-color-primary);
font-weight: 500;
}
.point-card-range {
color: var(--el-text-color-secondary);
}
}
}
} }
} }
} }

@ -1,5 +1,5 @@
<template> <template>
<Dialog v-model="dialogVisible" :title="dialogTitle" width="1100" :scroll="true" max-height="80vh" align-center> <Dialog v-if="mode === 'dialog'" v-model="dialogVisible" :title="dialogTitle" width="1100" :scroll="true" max-height="80vh" align-center>
<div class="single-device-dialog"> <div class="single-device-dialog">
<el-form class="-mb-15px history-single-device-form" :inline="true" label-width="auto"> <el-form class="-mb-15px history-single-device-form" :inline="true" label-width="auto">
<el-form-item :label="t('DataCollection.HistoryData.dialogCollectionTimeLabel')"> <el-form-item :label="t('DataCollection.HistoryData.dialogCollectionTimeLabel')">
@ -85,6 +85,91 @@ v-for="col in section.columns" :key="col.prop" :prop="col.prop" :label="col.labe
/> />
</template> </template>
</Dialog> </Dialog>
<ContentWrap v-else>
<div class="single-device-dialog">
<el-form class="-mb-15px history-single-device-form" :inline="true" label-width="auto">
<el-form-item :label="t('DataCollection.HistoryData.dialogCollectionTimeLabel')">
<el-date-picker
v-model="collectionTimeRange"
value-format="YYYY-MM-DD HH:mm:ss"
type="datetimerange"
:start-placeholder="t('DataCollection.HistoryData.dialogCollectionTimeStartPlaceholder')"
:end-placeholder="t('DataCollection.HistoryData.dialogCollectionTimeEndPlaceholder')"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-280px"
/>
</el-form-item>
<el-form-item :label="t('DataCollection.HistoryData.dialogPointFilterLabel')">
<el-select
v-model="attributeCodes"
class="!w-280px"
multiple
:max-collapse-tags="1"
collapse-tags
collapse-tags-tooltip
filterable
clearable
:loading="attributeLoading"
:placeholder="t('DataCollection.HistoryData.dialogPointFilterPlaceholder')"
>
<el-option
v-for="item in attributeOptions"
:key="item.attributeCode"
:label="item.label"
:value="item.attributeCode"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery">{{ t('DataCollection.HistoryData.dialogSearchButtonText') }}</el-button>
<el-button @click="resetQuery">{{ t('DataCollection.HistoryData.dialogResetButtonText') }}</el-button>
</el-form-item>
</el-form>
<ContentWrap v-loading="loading">
<div v-if="recordGroups.length" class="single-device-dialog__record-list">
<div v-for="group in recordGroups" :key="group.key" class="single-device-dialog__record">
<div class="single-device-dialog__record-title">
{{ t('DataCollection.HistoryData.dialogRecordCollectionTimePrefix') }}{{ group.collectTime || '-' }}
</div>
<div v-if="group.sections.length" class="single-device-dialog__table-grid">
<div
v-for="section in group.sections" :key="`${group.key}-${section.key}`"
class="single-device-dialog__section">
<div class="single-device-dialog__section-title">
{{ section.title }}
</div>
<el-empty
v-if="!section.columns.length"
:description="t('DataCollection.HistoryData.emptyDescription')"
/>
<el-table
v-else :data="section.rows" :border="true" :header-cell-style="headerCellStyle"
:cell-style="bodyCellStyle" size="small">
<el-table-column
v-for="col in section.columns" :key="col.prop" :prop="col.prop" :label="col.label"
align="center">
<template #default="scope">
<span>{{ formatCell(scope.row[col.prop]) }}</span>
</template>
</el-table-column>
</el-table>
</div>
</div>
<el-empty v-else :description="t('DataCollection.HistoryData.emptyDescription')" />
</div>
</div>
<el-empty v-else :description="t('DataCollection.HistoryData.emptyDescription')" />
</ContentWrap>
</div>
<Pagination
v-if="total > 0"
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="fetchHistory"
/>
</ContentWrap>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -116,7 +201,9 @@ const props = defineProps<{
modelValue: boolean modelValue: boolean
deviceId?: string | number deviceId?: string | number
deviceName?: string deviceName?: string
mode?: 'dialog' | 'inline'
}>() }>()
const mode = computed(() => props.mode ?? 'dialog')
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void (e: 'update:modelValue', value: boolean): void
@ -325,9 +412,12 @@ const resetQuery = () => {
} }
watch( watch(
() => [props.modelValue, props.deviceId], () => [props.modelValue, props.deviceId, mode.value],
([visible]) => { ([visible, deviceIdVal]) => {
if (!visible) { if (mode.value === 'dialog' && !visible) {
return
}
if (deviceIdVal === undefined || deviceIdVal === null || deviceIdVal === '') {
return return
} }
collectionTimeRange.value = buildLastHoursRange(4) collectionTimeRange.value = buildLastHoursRange(4)
@ -336,7 +426,8 @@ watch(
queryParams.pageSize = 10 queryParams.pageSize = 10
fetchDeviceAttributes() fetchDeviceAttributes()
fetchHistory() fetchHistory()
} },
{ immediate: true }
) )
</script> </script>

@ -1,4 +1,5 @@
<template> <template>
<div v-show="!showHistoryPage">
<ContentWrap> <ContentWrap>
<el-form <el-form
class="-mb-15px history-data-form" class="-mb-15px history-data-form"
@ -135,6 +136,28 @@
@pagination="getList" @pagination="getList"
/> />
</ContentWrap> </ContentWrap>
</div>
<!-- 历史记录整页 -->
<ContentWrap v-show="showHistoryPage">
<div class="mb-10px flex items-center justify-between text-sm text-gray-500">
<div>
<el-button type="primary" link @click="closeHistoryPage">
<Icon icon="ep:arrow-left" class="mr-5px" /> 返回历史记录列表
</el-button>
</div>
<div>
{{ t('DataCollection.HistoryData.dialogTitlePrefix') }}<span class="font-medium text-gray-700">{{ singleDeviceName || '-' }}</span>
</div>
<div></div>
</div>
<HistorySingleDeviceDialog
v-model="historyInlineVisible"
:device-id="singleDeviceId"
:device-name="singleDeviceName"
mode="inline"
/>
</ContentWrap>
<HistorySingleDeviceDialog <HistorySingleDeviceDialog
v-model="singleDialogVisible" v-model="singleDialogVisible"
@ -202,6 +225,8 @@ const handleSelectionChange = (rows: LineDeviceVO[]) => {
const singleDialogVisible = ref(false) const singleDialogVisible = ref(false)
const singleDeviceId = ref<string | number>() const singleDeviceId = ref<string | number>()
const singleDeviceName = ref<string>('') const singleDeviceName = ref<string>('')
const showHistoryPage = ref(false)
const historyInlineVisible = ref(false)
const buildQueryParams = (): LineDevicePageParams => { const buildQueryParams = (): LineDevicePageParams => {
const params: LineDevicePageParams = { const params: LineDevicePageParams = {
@ -254,7 +279,15 @@ const handleSingleView = (row: LineDeviceVO) => {
} }
singleDeviceId.value = deviceId singleDeviceId.value = deviceId
singleDeviceName.value = row.deviceName || '' singleDeviceName.value = row.deviceName || ''
singleDialogVisible.value = true historyInlineVisible.value = true
showHistoryPage.value = true
}
const closeHistoryPage = () => {
showHistoryPage.value = false
historyInlineVisible.value = false
singleDeviceId.value = ''
singleDeviceName.value = ''
} }
const handleSingleAnalyseView = (row: LineDeviceVO) => { const handleSingleAnalyseView = (row: LineDeviceVO) => {

Loading…
Cancel
Save