预警分析(日报月报和年报)制作 #54

Open
kangshuxin wants to merge 1 commits from YKS into master
  1. 73
      src/api/alarm/analysis/Day.ts
  2. 66
      src/api/alarm/analysis/Month.ts
  3. 66
      src/api/alarm/analysis/Year.ts
  4. 125
      src/views/alarm/analysis/day/CustomModal.vue
  5. 147
      src/views/alarm/analysis/day/Day.ts
  6. 79
      src/views/alarm/analysis/day/GzpDetailModal.vue
  7. 174
      src/views/alarm/analysis/day/index.vue
  8. 123
      src/views/alarm/analysis/month/CustomModal.vue
  9. 78
      src/views/alarm/analysis/month/GzpDetailModal.vue
  10. 149
      src/views/alarm/analysis/month/Month.ts
  11. 192
      src/views/alarm/analysis/month/index.vue
  12. 119
      src/views/alarm/analysis/year/CustomModal.vue
  13. 76
      src/views/alarm/analysis/year/GzpDetailModal.vue
  14. 145
      src/views/alarm/analysis/year/Year.ts
  15. 170
      src/views/alarm/analysis/year/index.vue

73
src/api/alarm/analysis/Day.ts

@ -0,0 +1,73 @@
import { formatToDateTime } from '@/utils/dateUtil'
import { defHttp } from '@/utils/http/axios'
// 类型定义
export interface AlarmRecord {
id: string
instanceName: string
totalAlarms: number
alarmDuration: string
hasDetails?: boolean
// 动态小时数据
hourCounts: { [key: `hour_${number}`]: number }
}
export interface AlarmApiResponse {
code: number
list: AlarmRecord[]
total: number
msg: string
}
export interface AlarmDetail {
tagname: string
alarmcount: number
alarmtime: number
}
export interface AlarmQueryParams {
unit?: string
driverType?: '模型' | '规则'
date?: string
}
// API接口
export function fetchAlarmData(params: AlarmQueryParams): Promise<AlarmApiResponse> {
return defHttp.get<AlarmApiResponse>({ url: '/alarm/daily-report', params })
}
export function fetchAlarmDetails(id: string, date?: string) {
return defHttp.get<AlarmDetail[]>({
url: `/alarm/details/${id}`,
params: { date }, // 传递日期参数
})
}
export interface GzpAlarmDetail {
gzpName: string
startTime: string
endTime: string
duration: number
}
export function fetchGzpAlarmDetails(
gzpName: string,
instanceId: string,
date?: string,
): Promise<GzpAlarmDetail[]> {
return defHttp.get({
url: `/alarm/gzp-details/${gzpName}`,
params: { instanceId, date },
}).then((res) => {
// 转换时间戳为格式化字符串
return res.map((item: any) => ({
gzpName: item.gzpName,
startTime: formatToDateTime(item.startTime),
endTime: formatToDateTime(item.endTime),
/* controller
msJava
LocalDateTime Spring JSON
*/
duration: item.duration,
}))
})
}

66
src/api/alarm/analysis/Month.ts

@ -0,0 +1,66 @@
import { formatToDateTime } from '@/utils/dateUtil'
import { defHttp } from '@/utils/http/axios'
export interface AlarmRecord {
id: string
instanceName: string
totalAlarms: number
alarmDuration: string
hasDetails?: boolean
dayCounts: { [key: `day_${number}`]: number }
}
export interface AlarmApiResponse {
code: number
list: AlarmRecord[]
total: number
msg: string
}
export interface AlarmDetail {
tagname: string
alarmcount: number
alarmtime: number
}
export interface AlarmQueryParams {
unit?: string
driverType?: '模型' | '规则'
month?: string
}
export interface GzpAlarmDetail {
gzpName: string
startTime: string
endTime: string
duration: number
}
export function fetchAlarmData(params: AlarmQueryParams): Promise<AlarmApiResponse> {
return defHttp.get<AlarmApiResponse>({ url: '/alarm/monthly-report', params })
}
export function fetchAlarmDetails(id: string, month?: string): Promise<AlarmDetail[]> {
return defHttp.get<AlarmDetail[]>({
url: `/alarm/monthly-details/${id}`,
params: { month },
})
}
export function fetchGzpAlarmDetails(
gzpName: string,
instanceId: string,
month?: string,
): Promise<GzpAlarmDetail[]> {
return defHttp.get({
url: `/alarm/monthly-gzp-details/${gzpName}`,
params: { instanceId, month },
}).then((res: any) => {
return res.map((item: any) => ({
gzpName: item.gzpName,
startTime: formatToDateTime(item.startTime),
endTime: formatToDateTime(item.endTime),
duration: item.duration,
}))
})
}

66
src/api/alarm/analysis/Year.ts

@ -0,0 +1,66 @@
import { formatToDateTime } from '@/utils/dateUtil'
import { defHttp } from '@/utils/http/axios'
export interface AlarmRecord {
id: string
instanceName: string
totalAlarms: number
alarmDuration: string
hasDetails?: boolean
monthCounts: { [key: `month_${number}`]: number }
}
export interface AlarmApiResponse {
code: number
list: AlarmRecord[]
total: number
msg: string
}
export interface AlarmDetail {
tagname: string
alarmcount: number
alarmtime: number
}
export interface AlarmQueryParams {
unit?: string
driverType?: '模型' | '规则'
year?: string
}
export function fetchAlarmData(params: AlarmQueryParams): Promise<AlarmApiResponse> {
return defHttp.get<AlarmApiResponse>({ url: '/alarm/yearly-report', params })
}
export function fetchAlarmDetails(id: string, year?: string) {
return defHttp.get<AlarmDetail[]>({
url: `/alarm/yearly-details/${id}`,
params: { year },
})
}
export interface GzpAlarmDetail {
gzpName: string
startTime: string
endTime: string
duration: number
}
export function fetchGzpAlarmDetails(
gzpName: string,
instanceId: string,
year?: string,
): Promise<GzpAlarmDetail[]> {
return defHttp.get({
url: `/alarm/yearly-gzp-details/${gzpName}`,
params: { instanceId, year },
}).then((res) => {
return res.map((item: any) => ({
gzpName: item.gzpName,
startTime: formatToDateTime(item.startTime),
endTime: formatToDateTime(item.endTime),
duration: item.duration,
}))
})
}

125
src/views/alarm/analysis/day/CustomModal.vue

@ -0,0 +1,125 @@
<script lang="ts" setup>
import { ref, watch } from 'vue'
import { Modal } from 'ant-design-vue'
import { detailColumns } from './Day'
import GzpModal from './GzpDetailModal.vue'
import { fetchAlarmDetails } from '@/api/alarm/analysis/Day'
import { BasicTable, useTable } from '@/components/Table'
import { useI18n } from '@/hooks/web/useI18n'
import { IconEnum } from '@/enums/appEnum'
import TableAction from '@/components/Table/src/components/TableAction.vue'
interface TableRecord {
id: string
tagname: string
alarmDuration: string
}
// antd
const props = defineProps({
visible: Boolean,
rec: {
type: Object as () => {
id: string
instanceName: string
date?: string
},
required: true,
},
})
const emit = defineEmits(['update:visible'])
const { t } = useI18n()
const gzpModalVisible = ref(false)
const currentGzpRecord = ref()
//
const [registerTable, { setTableData }] = useTable({
columns: detailColumns,
showIndexColumn: true,
pagination: true,
scroll: { x: 'max-content' },
actionColumn: {
width: 140,
title: t('action.detail'),
dataIndex: 'detail',
fixed: 'right',
},
})
const loading = ref(false)
const closeModal = () => emit('update:visible', false)
// record
watch(() => props.rec, (newVal) => {
if (newVal?.id && props.visible)
loadDetails(newVal.id, newVal.date)
}, { immediate: true })
async function loadDetails(id: string, date?: string) {
try {
loading.value = true
const data = await fetchAlarmDetails(id, date)
setTableData(data)
}
catch (error) {
console.error('加载详情失败:', error)
}
finally {
loading.value = false
}
}
function handleGzpDetail(record: TableRecord) {
currentGzpRecord.value = {
gzpName: record.tagname,
instanceId: props.rec.id,
date: props.rec.date, // index
}
gzpModalVisible.value = true
}
</script>
<template>
<Modal
:visible="visible"
width="80%"
:title="`${rec?.instanceName} - 报警详情`"
:footer="null"
:destroy-on-close="true"
@cancel="closeModal"
>
<div
style="height: calc(100vh - 450px); overflow: auto;"
>
<BasicTable
:loading="loading"
@register="registerTable"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'detail'">
<TableAction
:actions="[{
icon: IconEnum.PREVIEW,
label: t('action.detail'),
onClick: () => handleGzpDetail(record),
}]"
/>
</template>
</template>
</BasicTable>
</div>
</Modal>
<GzpModal
v-model:visible="gzpModalVisible"
:record="currentGzpRecord"
/>
</template>
<style scoped>
:deep(.ant-table-body) {
height: auto !important;
min-height: 200px;
max-height: 700px !important;
}
</style>

147
src/views/alarm/analysis/day/Day.ts

@ -0,0 +1,147 @@
import dayjs from 'dayjs'
import { h } from 'vue'
import type { BasicColumn, FormSchema } from '@/components/Table'
interface HourColumn extends BasicColumn {
dataIndex: `hour_${number}`
ifShow?: boolean
}
function generateHourColumns(): HourColumn[] {
return Array.from({ length: 24 }, (_, i) => ({
title: `${i}`,
dataIndex: `hour_${i}`,
width: 60,
align: 'center',
ifShow: true,
customRender: ({ text }: { text: number }) => {
if (text > 10)
return h('span', { style: { color: 'red', fontWeight: 'bold' } }, text)
else if (text > 0)
return h('span', { style: { color: 'blue' } }, text)
return text || 0
},
}))
}
export const columns: BasicColumn[] = [
{
title: '实例名称',
dataIndex: 'instanceName',
width: 180,
fixed: 'left',
},
...generateHourColumns(),
{
title: '总报警数量',
dataIndex: 'totalAlarms',
width: 100,
fixed: 'right',
sorter: true,
},
{
title: '报警时长',
dataIndex: 'alarmDuration',
width: 100,
fixed: 'right',
},
]
export const detailColumns: BasicColumn[] = [
{
title: '光字牌名称',
dataIndex: 'tagname',
width: 220,
},
{
title: '报警数量',
dataIndex: 'alarmcount',
width: 100,
},
{
title: '报警时长',
dataIndex: 'alarmtime',
width: 180,
},
]
interface SelectComponentProps {
options: Array<{ label: string, value: string }>
placeholder?: string
allowClear?: boolean
}
export const searchFormSchemas: FormSchema[] = [
{
field: 'unit',
label: '机组',
component: 'Select',
defaultValue: '1',
componentProps: {
options: [
{ label: '1号机组', value: '1' },
],
placeholder: '请选择机组',
allowClear: true,
} as SelectComponentProps,
colProps: { span: 5 },
},
{
field: 'driverType',
label: '驱动类型',
component: 'Select',
defaultValue: '模型',
componentProps: {
options: [
{ label: '模型驱动', value: '模型' },
{ label: '规则驱动', value: '规则' },
],
placeholder: '请选择驱动类型',
allowClear: true,
} as SelectComponentProps,
colProps: { span: 5 },
},
{
field: 'date',
label: '日期',
component: 'DatePicker',
defaultValue: dayjs().format('YYYY-MM-DD'),
componentProps: {
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD',
placeholder: '请选择日期',
allowClear: false,
},
colProps: { span: 8 },
},
]
export const gzpDetailColumns: BasicColumn[] = [
{
title: '光字牌名称',
dataIndex: 'gzpName',
width: 220,
},
{
title: '开始时间',
dataIndex: 'startTime',
width: 180,
},
{
title: '结束时间',
dataIndex: 'endTime',
width: 180,
},
{
title: '超限时长',
dataIndex: 'duration',
width: 120,
},
]

79
src/views/alarm/analysis/day/GzpDetailModal.vue

@ -0,0 +1,79 @@
<script lang="ts" setup>
import { ref, watch } from 'vue'
import { Modal } from 'ant-design-vue'
import { gzpDetailColumns } from './Day'
import { BasicTable, useTable } from '@/components/Table'
import { fetchGzpAlarmDetails } from '@/api/alarm/analysis/Day'
const props = defineProps({
visible: Boolean,
record: {
type: Object as () => {
gzpName: string
instanceId: string
date: string
},
required: true,
},
})
const emit = defineEmits(['update:visible'])
const [registerTable, { setTableData }] = useTable({
columns: gzpDetailColumns,
showIndexColumn: true,
pagination: true,
scroll: { x: 'max-content' },
})
const loading = ref(false)
const closeModal = () => emit('update:visible', false)
watch(() => props.record, (newVal) => {
if (newVal?.gzpName && props.visible)
loadDetails(newVal.gzpName, newVal.instanceId, newVal.date)
}, { immediate: true })
async function loadDetails(gzpName: string, instanceId: string, date?: string) {
try {
loading.value = true
const data = await fetchGzpAlarmDetails(gzpName, instanceId, date)
setTableData(data)
}
catch (error) {
console.error('加载光字牌详情失败:', error)
}
finally {
loading.value = false
}
}
</script>
<template>
<Modal
:visible="visible"
width="80%"
:title="`${record?.gzpName} - 报警详情`"
:footer="null"
:destroy-on-close="true"
@cancel="closeModal"
>
<div
style="height: calc(100vh - 450px); overflow: auto;"
>
<BasicTable
:loading="loading"
@register="registerTable"
/>
</div>
</Modal>
</template>
<style scoped>
:deep(.ant-table-body) {
height: auto !important;
min-height: 200px;
max-height: 700px !important;
}
</style>

174
src/views/alarm/analysis/day/index.vue

@ -0,0 +1,174 @@
<script lang="ts" setup>
import { ref } from 'vue'
import dayjs from 'dayjs'
import CustomModal from './CustomModal.vue'
import { columns, searchFormSchemas } from './Day'
import { fetchAlarmData } from '@/api/alarm/analysis/Day'
import { useI18n } from '@/hooks/web/useI18n'
import { BasicTable, useTable } from '@/components/Table'
import TableAction from '@/components/Table/src/components/TableAction.vue'
import { IconEnum } from '@/enums/appEnum'
const { t } = useI18n()
const modalVisible = ref(false)
const currentRecord = ref()
interface TableRecord {
id: string
instanceName: string
totalAlarms: number
alarmDuration: string
[key: `hour_${number}`]: number
}
const tableColumns = ref([...columns])
const [registerTable, { setColumns, getForm }] = useTable({
title: '报警分析日报表',
columns: tableColumns,
api: loadTableData,
rowKey: 'id',
showIndexColumn: true,
scroll: { x: 'max-content' },
useSearchForm: true,
formConfig: {
labelWidth: 100,
schemas: searchFormSchemas,
showResetButton: false,
},
actionColumn: {
width: 140,
title: t('action.detail'),
dataIndex: 'detail',
fixed: 'right',
},
})
async function loadTableData(params: any): Promise<TableRecord[]> {
const finalParams = {
...params, //
}
try {
const response = await fetchAlarmData(finalParams)
if (!response?.list)
return []
const rawData = response.list
const visibleHours = new Set<number>()
//
const summaryRow: TableRecord = {
id: 'summary',
instanceName: '合计',
totalAlarms: 0,
alarmDuration: '0',
...Object.fromEntries(
Array.from({ length: 24 }, (_, i) => [`hour_${i}`, 0]),
),
}
const transformedData = rawData.map((item) => {
const record: TableRecord = {
id: item.id,
instanceName: item.instanceName,
totalAlarms: item.totalAlarms,
alarmDuration: item.alarmDuration.toString(),
...Object.fromEntries(
Array.from({ length: 24 }, (_, i) => [`hour_${i}`, 0]),
),
}
if (item.hourCounts) {
Object.entries(item.hourCounts).forEach(([key, value]) => {
const hourKey = key as `hour_${number}`
const hourValue = Number(value) || 0
record[hourKey] = hourValue
//
summaryRow[hourKey] = (summaryRow[hourKey] || 0) + hourValue
if (hourValue > 0)
visibleHours.add(Number.parseInt(key.replace('hour_', '')))
})
}
//
summaryRow.totalAlarms = (summaryRow.totalAlarms || 0) + (record.totalAlarms || 0)
//
summaryRow.alarmDuration = (Number.parseInt(summaryRow.alarmDuration) + Number.parseInt(record.alarmDuration || '0')).toString()
return record
})
// 0
const shouldShowSummary
= summaryRow.totalAlarms > 0
|| Number.parseInt(summaryRow.alarmDuration) > 0
|| Object.values(summaryRow).some((value, index) => {
// 4id, instanceName, totalAlarms, alarmDuration
return index > 3 && typeof value === 'number' && value > 0
})
const newColumns = [
...columns.slice(0, 1),
...columns.slice(1, 25).filter((_, index) => visibleHours.has(index)),
...columns.slice(25).filter(Boolean),
]
tableColumns.value = newColumns
setColumns(newColumns)
//
return shouldShowSummary ? [summaryRow, ...transformedData] : transformedData.length ? transformedData : []
}
catch (error) {
console.error('加载数据出错:', error)
return []
}
}
function handleDetail(record: TableRecord) {
const form = getForm()
const currentDate = form?.getFieldsValue()?.date
currentRecord.value = {
id: record.id,
instanceName: record.instanceName,
date: currentDate, //
}
modalVisible.value = true
}
</script>
<template>
<div class="h-full flex flex-col p-4" style="height: calc(100vh - 200px); padding: 0; overflow: auto;">
<div class="flex-1 overflow-hidden">
<BasicTable @register="registerTable">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'detail'">
<TableAction
:actions="[{
icon: IconEnum.PREVIEW,
label: t('action.detail'),
onClick: () => handleDetail(record),
disabled: record.id === 'summary',
}]"
/>
</template>
</template>
</BasicTable>
<CustomModal
v-model:visible="modalVisible"
:rec="currentRecord"
/>
</div>
</div>
</template>
<style scoped>
:deep(.ant-table-body) {
height: auto !important;
min-height: 200px;
max-height: 700px !important;
}
</style>

123
src/views/alarm/analysis/month/CustomModal.vue

@ -0,0 +1,123 @@
<script lang="ts" setup>
import { ref, watch } from 'vue'
import { Modal } from 'ant-design-vue'
import { detailColumns } from './Month'
import GzpModal from './GzpDetailModal.vue'
import { fetchAlarmDetails } from '@/api/alarm/analysis/Month'
import { BasicTable, useTable } from '@/components/Table'
import { useI18n } from '@/hooks/web/useI18n'
import { IconEnum } from '@/enums/appEnum'
import TableAction from '@/components/Table/src/components/TableAction.vue'
interface TableRecord {
id: string
tagname: string
alarmDuration: string
alarmcount: number
}
const props = defineProps({
visible: Boolean,
rec: {
type: Object as () => {
id: string
instanceName: string
month?: string
},
required: true,
},
})
const emit = defineEmits(['update:visible'])
const { t } = useI18n()
const gzpModalVisible = ref(false)
const currentGzpRecord = ref()
const [registerTable, { setTableData }] = useTable({
columns: detailColumns,
showIndexColumn: true,
pagination: true,
scroll: { x: 'max-content' },
actionColumn: {
width: 140,
title: t('action.detail'),
dataIndex: 'detail',
fixed: 'right',
},
})
const loading = ref(false)
const closeModal = () => emit('update:visible', false)
watch(() => props.rec, (newVal) => {
if (newVal?.id && props.visible)
loadDetails(newVal.id, newVal.month)
}, { immediate: true })
async function loadDetails(id: string, month?: string) {
try {
loading.value = true
const data = await fetchAlarmDetails(id, month)
setTableData(data)
}
catch (error) {
console.error('加载详情失败:', error)
}
finally {
loading.value = false
}
}
function handleGzpDetail(record: TableRecord) {
currentGzpRecord.value = {
gzpName: record.tagname,
instanceId: props.rec.id,
month: props.rec.month,
}
gzpModalVisible.value = true
}
</script>
<template>
<Modal
:open="visible"
width="80%"
:title="`${rec?.instanceName} - 报警详情`"
:footer="null"
:destroy-on-close="true"
@cancel="closeModal"
>
<div
style="height: calc(100vh - 450px); overflow: auto;"
>
<BasicTable
:loading="loading"
@register="registerTable"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'detail'">
<TableAction
:actions="[{
icon: IconEnum.PREVIEW,
label: t('action.detail'),
onClick: () => handleGzpDetail(record),
}]"
/>
</template>
</template>
</BasicTable>
</div>
</Modal>
<GzpModal
v-model:visible="gzpModalVisible"
:record="currentGzpRecord"
/>
</template>
<style scoped>
:deep(.ant-table-body) {
height: auto !important;
min-height: 200px;
max-height: 700px !important;
}
</style>

78
src/views/alarm/analysis/month/GzpDetailModal.vue

@ -0,0 +1,78 @@
<script lang="ts" setup>
import { ref, watch } from 'vue'
import { Modal } from 'ant-design-vue'
import { gzpDetailColumns } from './Month'
import { BasicTable, useTable } from '@/components/Table'
import { fetchGzpAlarmDetails } from '@/api/alarm/analysis/Month'
const props = defineProps({
visible: Boolean,
record: {
type: Object as () => {
gzpName: string
instanceId: string
month: string
},
required: true,
},
})
const emit = defineEmits(['update:visible'])
const [registerTable, { setTableData }] = useTable({
columns: gzpDetailColumns,
showIndexColumn: true,
pagination: true,
scroll: { x: 'max-content' },
})
const loading = ref(false)
const closeModal = () => emit('update:visible', false)
watch(() => props.record, (newVal) => {
if (newVal?.gzpName && props.visible)
loadDetails(newVal.gzpName, newVal.instanceId, newVal.month)
}, { immediate: true })
async function loadDetails(gzpName: string, instanceId: string, month?: string) {
try {
loading.value = true
const data = await fetchGzpAlarmDetails(gzpName, instanceId, month)
setTableData(data)
}
catch (error) {
console.error('加载光字牌详情失败:', error)
}
finally {
loading.value = false
}
}
</script>
<template>
<Modal
:open="visible"
width="80%"
:title="`${record?.gzpName} - 报警详情`"
:footer="null"
:destroy-on-close="true"
@cancel="closeModal"
>
<div
style="height: calc(100vh - 450px); overflow: auto;"
>
<BasicTable
:loading="loading"
@register="registerTable"
/>
</div>
</Modal>
</template>
<style scoped>
:deep(.ant-table-body) {
height: auto !important;
min-height: 200px;
max-height: 700px !important;
}
</style>

149
src/views/alarm/analysis/month/Month.ts

@ -0,0 +1,149 @@
import dayjs from 'dayjs'
import { h } from 'vue'
import type { BasicColumn, FormSchema } from '@/components/Table'
interface DayColumn extends BasicColumn {
dataIndex: `day_${number}`
ifShow?: boolean
}
function generateDayColumns(year: number, month: number): DayColumn[] {
const daysInMonth = dayjs(`${year}-${month}`).daysInMonth()
return Array.from({ length: daysInMonth }, (_, i) => {
const day = i + 1
return {
title: `${day}`,
dataIndex: `day_${day}`,
width: 60,
align: 'center',
ifShow: true,
customRender: ({ text }: { text: number }) => {
if (text > 10)
return h('span', { style: { color: 'red', fontWeight: 'bold' } }, text)
else if (text > 0)
return h('span', { style: { color: 'blue' } }, text)
return text || 0
},
}
})
}
export const columns: BasicColumn[] = [
{
title: '实例名称',
dataIndex: 'instanceName',
width: 180,
fixed: 'left',
},
{
title: '总报警数量',
dataIndex: 'totalAlarms',
width: 100,
fixed: 'right',
sorter: true,
},
{
title: '报警时长',
dataIndex: 'alarmDuration',
width: 100,
fixed: 'right',
},
]
export const detailColumns: BasicColumn[] = [
{
title: '光字牌名称',
dataIndex: 'tagname',
width: 220,
fixed: 'left',
},
{
title: '报警数量',
dataIndex: 'alarmcount',
width: 100,
},
{
title: '报警时长',
dataIndex: 'alarmtime',
width: 120,
},
]
interface SelectComponentProps {
options: Array<{ label: string, value: string }>
placeholder?: string
allowClear?: boolean
}
export const searchFormSchemas: FormSchema[] = [
{
field: 'unit',
label: '机组',
component: 'Select',
defaultValue: '1',
componentProps: {
options: [
{ label: '1号机组', value: '1' },
],
placeholder: '请选择机组',
allowClear: true,
} as SelectComponentProps,
colProps: { span: 5 },
},
{
field: 'driverType',
label: '驱动类型',
component: 'Select',
defaultValue: '模型',
componentProps: {
options: [
{ label: '模型驱动', value: '模型' },
{ label: '规则驱动', value: '规则' },
],
placeholder: '请选择驱动类型',
allowClear: true,
} as SelectComponentProps,
colProps: { span: 5 },
},
{
field: 'month',
label: '月份',
component: 'MonthPicker',
defaultValue: dayjs().format('YYYY-MM'),
componentProps: {
format: 'YYYY-MM',
valueFormat: 'YYYY-MM',
placeholder: '请选择月份',
allowClear: false,
},
colProps: { span: 8 },
},
]
export const gzpDetailColumns: BasicColumn[] = [
{
title: '光字牌名称',
dataIndex: 'gzpName',
width: 200,
fixed: 'left',
},
{
title: '开始时间',
dataIndex: 'startTime',
width: 180,
},
{
title: '结束时间',
dataIndex: 'endTime',
width: 180,
},
{
title: '超限时长',
dataIndex: 'duration',
width: 120,
},
]
export { generateDayColumns }

192
src/views/alarm/analysis/month/index.vue

@ -0,0 +1,192 @@
<script lang="ts" setup>
import { ref } from 'vue'
import dayjs from 'dayjs'
import CustomModal from './CustomModal.vue'
import { columns, generateDayColumns, searchFormSchemas } from './Month'
import { fetchAlarmData } from '@/api/alarm/analysis/Month'
import { useI18n } from '@/hooks/web/useI18n'
import { BasicTable, useTable } from '@/components/Table'
import TableAction from '@/components/Table/src/components/TableAction.vue'
import { IconEnum } from '@/enums/appEnum'
const { t } = useI18n()
const modalVisible = ref(false)
const currentRecord = ref()
interface TableRecord {
id: string
instanceName: string
totalAlarms: number
alarmDuration: string
[key: `day_${number}`]: number
}
const tableColumns = ref([...columns])
const [registerTable, { setColumns, getForm }] = useTable({
title: '报警分析月报表',
columns: tableColumns,
api: loadTableData,
rowKey: 'id',
showIndexColumn: true,
scroll: { x: 'max-content' },
useSearchForm: true,
formConfig: {
labelWidth: 100,
schemas: searchFormSchemas,
showResetButton: false,
},
actionColumn: {
width: 140,
title: t('action.detail'),
dataIndex: 'detail',
fixed: 'right',
},
})
async function loadTableData(params: any): Promise<TableRecord[]> {
const finalParams = {
...params, //
}
try {
const response = await fetchAlarmData(finalParams)
if (!response?.list)
return []
const rawData = response.list
const [year, month] = finalParams.month.split('-').map(Number)
const daysInMonth = dayjs(finalParams.month).daysInMonth()
const visibleDays = new Set<number>()
//
const summaryRow: TableRecord = {
id: 'summary',
instanceName: '合计',
totalAlarms: 0,
alarmDuration: '0',
...Object.fromEntries(
Array.from({ length: daysInMonth }, (_, i) => [`day_${i + 1}`, 0]),
),
}
const transformedData = rawData.map((item) => {
const record: TableRecord = {
id: item.id,
instanceName: item.instanceName,
totalAlarms: item.totalAlarms,
alarmDuration: item.alarmDuration.toString(),
...Object.fromEntries(
Array.from({ length: daysInMonth }, (_, i) => [`day_${i + 1}`, 0]),
),
}
if (item.dayCounts) {
Object.entries(item.dayCounts).forEach(([key, value]) => {
const dayKey = key as `day_${number}`
const dayValue = Number(value) || 0
record[dayKey] = dayValue
//
summaryRow[dayKey] = (summaryRow[dayKey] || 0) + dayValue
if (dayValue > 0) {
const day = Number.parseInt(key.replace('day_', ''))
visibleDays.add(day)
}
})
}
//
summaryRow.totalAlarms = (summaryRow.totalAlarms || 0) + (record.totalAlarms || 0)
//
summaryRow.alarmDuration = (Number.parseInt(summaryRow.alarmDuration) + Number.parseInt(record.alarmDuration || '0')).toString()
return record
})
//
const shouldShowSummary
= summaryRow.totalAlarms > 0
|| Number.parseInt(summaryRow.alarmDuration) > 0
|| Object.values(summaryRow).some((value, index) => {
// 4
return index > 3 && typeof value === 'number' && value > 0
})
//
const dayColumns = generateDayColumns(year, month)
const visibleDayColumns = dayColumns.filter((col) => {
const day = Number.parseInt(col.dataIndex.replace('day_', ''))
return visibleDays.has(day)
})
const newColumns = [
columns[0], //
...visibleDayColumns, //
columns[1], //
columns[2], //
]
tableColumns.value = newColumns
setColumns(newColumns)
//
return shouldShowSummary ? [summaryRow, ...transformedData] : transformedData.length ? transformedData : []
}
catch (error) {
console.error('加载数据出错:', error)
return []
}
}
function handleDetail(record: TableRecord) {
const form = getForm()
const currentMonth = form?.getFieldsValue()?.month
currentRecord.value = {
id: record.id,
instanceName: record.instanceName,
month: currentMonth, //
}
modalVisible.value = true
}
</script>
<template>
<div class="h-full flex flex-col p-4" style="height: calc(100vh - 200px); padding: 0; overflow: auto;">
<div class="flex-1 overflow-hidden">
<BasicTable @register="registerTable">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'detail'">
<TableAction
:actions="[{
icon: IconEnum.PREVIEW,
label: t('action.detail'),
onClick: () => handleDetail(record),
disabled: record.id === 'summary',
}]"
/>
</template>
</template>
</BasicTable>
<CustomModal
v-model:visible="modalVisible"
:rec="currentRecord"
/>
</div>
</div>
</template>
<style scoped>
:deep(.ant-table-body) {
height: auto !important;
min-height: 200px;
max-height: 700px !important;
}
:deep(.ant-table-cell) {
padding: 8px 4px !important;
}
</style>

119
src/views/alarm/analysis/year/CustomModal.vue

@ -0,0 +1,119 @@
<script lang="ts" setup>
import { ref, watch } from 'vue'
import { Modal } from 'ant-design-vue'
import { detailColumns } from './Year'
import GzpModal from './GzpDetailModal.vue'
import { fetchAlarmDetails } from '@/api/alarm/analysis/Year'
import { BasicTable, useTable } from '@/components/Table'
import { useI18n } from '@/hooks/web/useI18n'
import { IconEnum } from '@/enums/appEnum'
import TableAction from '@/components/Table/src/components/TableAction.vue'
interface TableRecord {
id: string
tagname: string
alarmDuration: string
}
const props = defineProps({
visible: Boolean,
rec: {
type: Object as () => {
id: string
instanceName: string
year?: string
},
required: true,
},
})
const emit = defineEmits(['update:visible'])
const { t } = useI18n()
const gzpModalVisible = ref(false)
const currentGzpRecord = ref()
const [registerTable, { setTableData }] = useTable({
columns: detailColumns,
showIndexColumn: true,
pagination: true,
scroll: { x: 'max-content' },
actionColumn: {
width: 140,
title: t('action.detail'),
dataIndex: 'detail',
fixed: 'right',
},
})
const loading = ref(false)
const closeModal = () => emit('update:visible', false)
watch(() => props.rec, (newVal) => {
if (newVal?.id && props.visible)
loadDetails(newVal.id, newVal.year)
}, { immediate: true })
async function loadDetails(id: string, year?: string) {
try {
loading.value = true
const data = await fetchAlarmDetails(id, year)
setTableData(data)
}
catch (error) {
console.error('加载详情失败:', error)
}
finally {
loading.value = false
}
}
function handleGzpDetail(record: TableRecord) {
currentGzpRecord.value = {
gzpName: record.tagname,
instanceId: props.rec.id,
year: props.rec.year,
}
gzpModalVisible.value = true
}
</script>
<template>
<Modal
:visible="visible"
width="80%"
:title="`${rec?.instanceName} - 报警详情`"
:footer="null"
:destroy-on-close="true"
@cancel="closeModal"
>
<div style="height: calc(100vh - 450px); overflow: auto;">
<BasicTable
:loading="loading"
@register="registerTable"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'detail'">
<TableAction
:actions="[{
icon: IconEnum.PREVIEW,
label: t('action.detail'),
onClick: () => handleGzpDetail(record),
}]"
/>
</template>
</template>
</BasicTable>
</div>
</Modal>
<GzpModal
v-model:visible="gzpModalVisible"
:record="currentGzpRecord"
/>
</template>
<style scoped>
:deep(.ant-table-body) {
height: auto !important;
min-height: 200px;
max-height: 700px !important;
}
</style>

76
src/views/alarm/analysis/year/GzpDetailModal.vue

@ -0,0 +1,76 @@
<script lang="ts" setup>
import { ref, watch } from 'vue'
import { Modal } from 'ant-design-vue'
import { gzpDetailColumns } from './Year'
import { BasicTable, useTable } from '@/components/Table'
import { fetchGzpAlarmDetails } from '@/api/alarm/analysis/Year'
const props = defineProps({
visible: Boolean,
record: {
type: Object as () => {
gzpName: string
instanceId: string
year: string
},
required: true,
},
})
const emit = defineEmits(['update:visible'])
const [registerTable, { setTableData }] = useTable({
columns: gzpDetailColumns,
showIndexColumn: true,
pagination: true,
scroll: { x: 'max-content' },
})
const loading = ref(false)
const closeModal = () => emit('update:visible', false)
watch(() => props.record, (newVal) => {
if (newVal?.gzpName && props.visible)
loadDetails(newVal.gzpName, newVal.instanceId, newVal.year)
}, { immediate: true })
async function loadDetails(gzpName: string, instanceId: string, year?: string) {
try {
loading.value = true
const data = await fetchGzpAlarmDetails(gzpName, instanceId, year)
setTableData(data)
}
catch (error) {
console.error('加载光字牌详情失败:', error)
}
finally {
loading.value = false
}
}
</script>
<template>
<Modal
:visible="visible"
width="80%"
:title="`${record?.gzpName} - 报警详情`"
:footer="null"
:destroy-on-close="true"
@cancel="closeModal"
>
<div style="height: calc(100vh - 450px); overflow: auto;">
<BasicTable
:loading="loading"
@register="registerTable"
/>
</div>
</Modal>
</template>
<style scoped>
:deep(.ant-table-body) {
height: auto !important;
min-height: 200px;
max-height: 700px !important;
}
</style>

145
src/views/alarm/analysis/year/Year.ts

@ -0,0 +1,145 @@
import dayjs from 'dayjs'
import { h } from 'vue'
import type { BasicColumn, FormSchema } from '@/components/Table'
interface MonthColumn extends BasicColumn {
dataIndex: `month_${number}`
ifShow?: boolean
}
function generateMonthColumns(): MonthColumn[] {
return Array.from({ length: 12 }, (_, i) => ({
title: `${i + 1}`,
dataIndex: `month_${i + 1}`,
width: 60,
align: 'center',
ifShow: true,
customRender: ({ text }: { text: number }) => {
if (text > 10)
return h('span', { style: { color: 'red' } }, text)
else if (text > 0 && text <= 10)
return h('span', { style: { color: 'blue' } }, text)
return text
},
}))
}
export const columns: BasicColumn[] = [
{
title: '实例名称',
dataIndex: 'instanceName',
width: 180,
fixed: 'left',
},
...generateMonthColumns(),
{
title: '总报警数量',
dataIndex: 'totalAlarms',
width: 100,
fixed: 'right',
sorter: true,
},
{
title: '报警时长',
dataIndex: 'alarmDuration',
width: 100,
fixed: 'right',
},
]
export const detailColumns: BasicColumn[] = [
{
title: '光字牌名称',
dataIndex: 'tagname',
width: 220,
},
{
title: '报警数量',
dataIndex: 'alarmcount',
width: 100,
},
{
title: '报警时长',
dataIndex: 'alarmtime',
width: 180,
},
]
interface SelectComponentProps {
options: Array<{ label: string, value: string }>
placeholder?: string
allowClear?: boolean
}
export const searchFormSchemas: FormSchema[] = [
{
field: 'unit',
label: '机组',
component: 'Select',
defaultValue: '1',
componentProps: {
options: [
{ label: '1号机组', value: '1' },
],
placeholder: '请选择机组',
allowClear: true,
} as SelectComponentProps,
colProps: { span: 5 },
},
{
field: 'driverType',
label: '驱动类型',
component: 'Select',
defaultValue: '模型',
componentProps: {
options: [
{ label: '模型驱动', value: '模型' },
{ label: '规则驱动', value: '规则' },
],
placeholder: '请选择驱动类型',
allowClear: true,
} as SelectComponentProps,
colProps: { span: 5 },
},
{
field: 'year',
label: '年份',
component: 'DatePicker',
defaultValue: dayjs().format('YYYY'),
componentProps: {
format: 'YYYY',
valueFormat: 'YYYY',
placeholder: '请选择年份',
allowClear: false,
picker: 'year',
},
colProps: { span: 8 },
},
]
export const gzpDetailColumns: BasicColumn[] = [
{
title: '光字牌名称',
dataIndex: 'gzpName',
width: 220,
},
{
title: '开始时间',
dataIndex: 'startTime',
width: 180,
},
{
title: '结束时间',
dataIndex: 'endTime',
width: 180,
},
{
title: '超限时长',
dataIndex: 'duration',
width: 120,
},
]

170
src/views/alarm/analysis/year/index.vue

@ -0,0 +1,170 @@
<script lang="ts" setup>
import { ref } from 'vue'
import CustomModal from './CustomModal.vue'
import { columns, searchFormSchemas } from './Year'
import { fetchAlarmData } from '@/api/alarm/analysis/Year'
import { useI18n } from '@/hooks/web/useI18n'
import { BasicTable, useTable } from '@/components/Table'
import TableAction from '@/components/Table/src/components/TableAction.vue'
import { IconEnum } from '@/enums/appEnum'
const { t } = useI18n()
const modalVisible = ref(false)
const currentRecord = ref()
interface TableRecord {
id: string
instanceName: string
totalAlarms: number
alarmDuration: string
[key: `month_${number}`]: number
}
const tableColumns = ref([...columns])
const [registerTable, { setColumns, getForm }] = useTable({
title: '报警分析年报表',
columns: tableColumns,
api: loadTableData,
rowKey: 'id',
showIndexColumn: true,
scroll: { x: 'max-content' },
useSearchForm: true,
formConfig: {
labelWidth: 100,
schemas: searchFormSchemas,
showResetButton: false,
},
actionColumn: {
width: 140,
title: t('action.detail'),
dataIndex: 'detail',
fixed: 'right',
},
})
async function loadTableData(params: any): Promise<TableRecord[]> {
const finalParams = {
...params,
}
try {
const response = await fetchAlarmData(finalParams)
if (!response?.list)
return []
const rawData = response.list
const visibleMonths = new Set<number>()
const summaryRow: TableRecord = {
id: 'summary',
instanceName: '合计',
totalAlarms: 0,
alarmDuration: '0',
...Object.fromEntries(
Array.from({ length: 12 }, (_, i) => [`month_${i + 1}`, 0]),
),
}
const transformedData = rawData.map((item) => {
const record: TableRecord = {
id: item.id,
instanceName: item.instanceName,
totalAlarms: item.totalAlarms,
alarmDuration: item.alarmDuration.toString(),
...Object.fromEntries(
Array.from({ length: 12 }, (_, i) => [`month_${i + 1}`, 0]),
),
}
if (item.monthCounts) {
Object.entries(item.monthCounts).forEach(([key, value]) => {
const monthKey = key as `month_${number}`
const monthValue = Number(value) || 0
record[monthKey] = monthValue
summaryRow[monthKey] = (summaryRow[monthKey] || 0) + monthValue
if (monthValue > 0) {
const monthNum = Number.parseInt(key.replace('month_', ''))
visibleMonths.add(monthNum)
}
})
}
summaryRow.totalAlarms = (summaryRow.totalAlarms || 0) + (record.totalAlarms || 0)
summaryRow.alarmDuration = (Number.parseInt(summaryRow.alarmDuration) + Number.parseInt(record.alarmDuration || '0')).toString()
return record
})
const shouldShowSummary
= summaryRow.totalAlarms > 0
|| Number.parseInt(summaryRow.alarmDuration) > 0
|| Object.values(summaryRow).some((value, index) => {
return index > 3 && typeof value === 'number' && value > 0
})
const newColumns = [
...columns.slice(0, 1),
...columns.slice(1, 13).filter((_, index) => visibleMonths.has(index + 1)),
...columns.slice(13).filter(Boolean),
]
tableColumns.value = newColumns
setColumns(newColumns)
return shouldShowSummary ? [summaryRow, ...transformedData] : transformedData.length ? transformedData : []
}
catch (error) {
console.error('加载数据出错:', error)
return []
}
}
async function handleDetail(record: TableRecord) {
const form = getForm()
const currentYear = form?.getFieldsValue()?.year
currentRecord.value = {
id: record.id,
instanceName: record.instanceName,
year: currentYear,
}
modalVisible.value = true
}
</script>
<template>
<div class="h-full flex flex-col p-4" style="height: calc(100vh - 200px); padding: 0; overflow: auto;">
<div class="flex-1 overflow-hidden">
<BasicTable @register="registerTable">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'detail'">
<TableAction
:actions="[{
icon: IconEnum.PREVIEW,
label: t('action.detail'),
onClick: () => handleDetail(record),
disabled: record.id === 'summary',
}]"
/>
</template>
</template>
</BasicTable>
<CustomModal
v-model:visible="modalVisible"
:rec="currentRecord"
/>
</div>
</div>
</template>
<style scoped>
:deep(.ant-table-body) {
height: auto !important;
min-height: 200px;
max-height: 700px !important;
}
</style>
Loading…
Cancel
Save