You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

300 lines
8.4 KiB
Vue

<template>
<Dialog v-model="dialogVisible" :title="dialogTitle" width="900" :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')">
<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-360px"
/>
</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>
</Dialog>
</template>
<script setup lang="ts">
import { DeviceApi } from '@/api/iot/device'
type SectionColumn = {
prop: string
label: string
}
type SectionRow = Record<string, string | number | null>
type Section = {
key: string
title: string
columns: SectionColumn[]
rows: SectionRow[]
}
type RecordGroup = {
key: string
collectTime?: string
sections: Section[]
}
const { t } = useI18n()
const props = defineProps<{
modelValue: boolean
deviceId?: string | number
deviceName?: string
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void
}>()
const dialogVisible = computed({
get() {
return props.modelValue
},
set(value: boolean) {
emit('update:modelValue', value)
}
})
const dialogTitle = computed(() => {
const name = props.deviceName ? props.deviceName : '-'
return `${t('DataCollection.HistoryData.dialogTitlePrefix')}${name}`
})
const loading = ref(false)
const recordGroups = ref<RecordGroup[]>([])
const collectionTimeRange = ref<string[]>([])
const formatDateTime = (date: Date) => {
const pad = (value: number) => String(value).padStart(2, '0')
const y = date.getFullYear()
const m = pad(date.getMonth() + 1)
const d = pad(date.getDate())
const h = pad(date.getHours())
const mi = pad(date.getMinutes())
const s = pad(date.getSeconds())
return `${y}-${m}-${d} ${h}:${mi}:${s}`
}
const buildLastHoursRange = (hours: number) => {
const end = new Date()
const start = new Date(end.getTime() - hours * 60 * 60 * 1000)
return [formatDateTime(start), formatDateTime(end)]
}
const toGroupMap = (value: any): Record<string, any[]> => {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return {}
}
const entries = Object.entries(value).filter(([, v]) => Array.isArray(v))
if (!entries.length) {
return {}
}
const map: Record<string, any[]> = {}
for (const [k, v] of entries) {
map[k] = v as any[]
}
return map
}
const formatCell = (value: string | number | null | undefined) => {
if (value === null || value === undefined) {
return ''
}
return value
}
const headerCellStyle = () => {
return {
fontWeight: 500,
padding: '6px 4px',
backgroundColor: 'var(--el-fill-color-light)',
color: 'var(--el-text-color-primary)'
}
}
const bodyCellStyle = () => {
return {
padding: '4px 4px'
}
}
const buildSectionsFromGroups = (groups: Record<string, any[]>): Section[] => {
const result: Section[] = []
for (const [key, list] of Object.entries(groups)) {
const columns: SectionColumn[] = []
const row: SectionRow = {}
if (Array.isArray(list) && list.length) {
list.forEach((item: any, index: number) => {
const prop = `col${index}`
const label =
item?.attributeName ?? `${t('DataCollection.HistoryData.defaultFieldLabelPrefix')}${index + 1}`
columns.push({ prop, label })
row[prop] = item?.addressValue ?? ''
})
}
result.push({
key,
title: key,
columns,
rows: columns.length ? [row] : []
})
}
return result
}
const fetchHistory = async () => {
if (props.deviceId === undefined || props.deviceId === null || props.deviceId === '') {
recordGroups.value = []
return
}
loading.value = true
try {
const params: Parameters<typeof DeviceApi.getHistoryRecord>[0] = { deviceId: props.deviceId }
if (Array.isArray(collectionTimeRange.value) && collectionTimeRange.value.length === 2) {
params.collectionStartTime = collectionTimeRange.value[0]
params.collectionEndTime = collectionTimeRange.value[1]
}
const res: any = await DeviceApi.getHistoryRecord(params)
const list = res?.data?.data ?? res?.data ?? res
const records = Array.isArray(list) ? list : []
recordGroups.value = records.map((item: any, index: number) => {
const groups = toGroupMap(item)
return {
key: `${item?.collectTime ?? index}-${index}`,
collectTime: item?.collectTime,
sections: buildSectionsFromGroups(groups)
}
})
} finally {
loading.value = false
}
}
const handleQuery = () => {
fetchHistory()
}
const resetQuery = () => {
collectionTimeRange.value = []
fetchHistory()
}
watch(
() => [props.modelValue, props.deviceId],
([visible]) => {
if (!visible) {
return
}
collectionTimeRange.value = buildLastHoursRange(4)
fetchHistory()
}
)
</script>
<style scoped>
.single-device-dialog {
display: flex;
flex-direction: column;
gap: 12px;
}
.single-device-dialog__table-grid {
display: grid;
grid-template-columns: repeat(1, minmax(0, 1fr));
gap: 12px;
}
.single-device-dialog__record-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.single-device-dialog__record {
padding: 12px;
background: var(--el-bg-color-overlay);
border: 1px solid var(--el-border-color);
border-radius: 6px;
}
.single-device-dialog__section {
padding: 10px;
background: var(--el-fill-color-lighter);
border: 1px solid var(--el-border-color);
border-radius: 6px;
}
.single-device-dialog__section :deep(.el-table) {
--el-table-row-hover-bg-color: transparent;
}
.single-device-dialog__record-title {
margin-bottom: 6px;
font-size: 13px;
color: var(--el-text-color-secondary);
}
.single-device-dialog__section-title {
margin-bottom: 4px;
font-size: 13px;
color: var(--el-text-color-primary);
}
.single-device-dialog__section :deep(.el-table__inner-wrapper) {
border-radius: 0;
}
.single-device-dialog__section :deep(.el-table__header-wrapper th) {
border-bottom-color: var(--el-border-color);
}
</style>