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

main
zhongwenkai 1 day ago
parent 9936bdb0d3
commit 8a2e5d9104

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

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

@ -125,33 +125,24 @@
</div>
<div class="point-list">
<el-collapse v-model="activePointGroup">
<el-collapse-item
v-for="group in filteredPointGroups"
:key="group.group"
:title="`${group.group} (${group.points.length})`"
:name="group.group"
>
<el-checkbox-group v-model="selectedPointCodes">
<el-space wrap>
<el-checkbox
v-for="point in group.points"
:key="point.code"
:label="point.code"
>
<div class="point-info">
<div class="point-name">{{ point.name }}</div>
<div class="point-detail">
<span class="point-code">{{ point.code }}</span>
<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 v-for="group in filteredPointGroups" :key="group.group" class="point-group-card">
<div class="point-group-header">{{ group.group }} ({{ group.points.length }})</div>
<div class="point-card-grid">
<div
v-for="point in group.points"
:key="point.code"
:class="['point-card', { 'point-card--selected': selectedPointCodes.includes(point.code) }]"
@click="togglePoint(point.code)"
>
<div class="point-card-name">{{ point.name }}</div>
<div class="point-card-code">{{ point.code }}</div>
<div class="point-card-meta">
<span class="point-card-unit">{{ point.unit }}</span>
<span class="point-card-range">{{ point.lower }} ~ {{ point.upper }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</el-form-item>
@ -465,6 +456,15 @@ const pointKeyword = ref('')
const activePointGroup = 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 allPoints = pointGroups.value.flatMap(g => g.points)
@ -1177,9 +1177,87 @@ window.addEventListener('resize', () => {
}
.point-list {
max-height: 230px;
max-height: 320px;
overflow: auto;
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>
<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">
<el-form class="-mb-15px history-single-device-form" :inline="true" label-width="auto">
<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>
</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>
<script setup lang="ts">
@ -116,7 +201,9 @@ const props = defineProps<{
modelValue: boolean
deviceId?: string | number
deviceName?: string
mode?: 'dialog' | 'inline'
}>()
const mode = computed(() => props.mode ?? 'dialog')
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void
@ -325,9 +412,12 @@ const resetQuery = () => {
}
watch(
() => [props.modelValue, props.deviceId],
([visible]) => {
if (!visible) {
() => [props.modelValue, props.deviceId, mode.value],
([visible, deviceIdVal]) => {
if (mode.value === 'dialog' && !visible) {
return
}
if (deviceIdVal === undefined || deviceIdVal === null || deviceIdVal === '') {
return
}
collectionTimeRange.value = buildLastHoursRange(4)
@ -336,7 +426,8 @@ watch(
queryParams.pageSize = 10
fetchDeviceAttributes()
fetchHistory()
}
},
{ immediate: true }
)
</script>

@ -1,4 +1,5 @@
<template>
<div v-show="!showHistoryPage">
<ContentWrap>
<el-form
class="-mb-15px history-data-form"
@ -135,6 +136,28 @@
@pagination="getList"
/>
</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
v-model="singleDialogVisible"
@ -202,6 +225,8 @@ const handleSelectionChange = (rows: LineDeviceVO[]) => {
const singleDialogVisible = ref(false)
const singleDeviceId = ref<string | number>()
const singleDeviceName = ref<string>('')
const showHistoryPage = ref(false)
const historyInlineVisible = ref(false)
const buildQueryParams = (): LineDevicePageParams => {
const params: LineDevicePageParams = {
@ -254,7 +279,15 @@ const handleSingleView = (row: LineDeviceVO) => {
}
singleDeviceId.value = deviceId
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) => {

Loading…
Cancel
Save