15 changed files with 1782 additions and 0 deletions
@ -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层传过来的日期格式是对的 |
|||
但前端收到的是ms(错误),可能是Java 后 |
|||
端返回的 LocalDateTime 对象被 Spring 默认的 JSON 序列化器转换成了时间戳 |
|||
故在前端进行时间转化 */ |
|||
duration: item.duration, |
|||
})) |
|||
}) |
|||
} |
@ -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, |
|||
})) |
|||
}) |
|||
} |
@ -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, |
|||
})) |
|||
}) |
|||
} |
@ -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> |
@ -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, |
|||
}, |
|||
] |
@ -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> |
@ -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) => { |
|||
// 检查所有小时列(跳过前4个固定列:id, 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> |
@ -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> |
@ -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> |
@ -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 } |
@ -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> |
@ -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> |
@ -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> |
@ -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, |
|||
}, |
|||
] |
@ -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…
Reference in new issue