feat(iot):增加 alert 模块的代码评审

pull/345/head
YunaiV 2026-05-18 08:48:25 +08:00
parent 6740401f6c
commit 58f8b7fb22
3 changed files with 68 additions and 145 deletions

View File

@ -1,8 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { AlertConfigApi } from '#/api/iot/alert/config'; import type { AlertConfig } from '#/api/iot/alert/config';
import { Page, useVbenModal } from '@vben/common-ui'; import { Page, useVbenModal } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import { getDictLabel } from '@vben/hooks';
import { message, Tag } from 'ant-design-vue'; import { message, Tag } from 'ant-design-vue';
@ -25,54 +27,18 @@ function handleRefresh() {
gridApi.query(); gridApi.query();
} }
//
function getLevelText(level?: number) {
const levelMap: Record<number, string> = {
1: '提示',
2: '一般',
3: '警告',
4: '严重',
5: '紧急',
};
return level ? levelMap[level] || `级别${level}` : '-';
}
//
function getLevelColor(level?: number) {
const colorMap: Record<number, string> = {
1: 'blue',
2: 'green',
3: 'orange',
4: 'red',
5: 'purple',
};
return level ? colorMap[level] || 'default' : 'default';
}
//
function getReceiveTypeText(type?: number) {
const typeMap: Record<number, string> = {
1: '站内信',
2: '邮箱',
3: '短信',
4: '微信',
5: '钉钉',
};
return type ? typeMap[type] || `类型${type}` : '-';
}
/** 创建告警配置 */ /** 创建告警配置 */
function handleCreate() { function handleCreate() {
formModalApi.setData(null).open(); formModalApi.setData(null).open();
} }
/** 编辑告警配置 */ /** 编辑告警配置 */
function handleEdit(row: AlertConfigApi.AlertConfig) { function handleEdit(row: AlertConfig) {
formModalApi.setData(row).open(); formModalApi.setData(row).open();
} }
/** 删除告警配置 */ /** 删除告警配置 */
async function handleDelete(row: AlertConfigApi.AlertConfig) { async function handleDelete(row: AlertConfig) {
const hideLoading = message.loading({ const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]), content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0, duration: 0,
@ -115,7 +81,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
refresh: true, refresh: true,
search: true, search: true,
}, },
} as VxeTableGridOptions<AlertConfigApi.AlertConfig>, } as VxeTableGridOptions<AlertConfig>,
}); });
</script> </script>
@ -130,24 +96,25 @@ const [Grid, gridApi] = useVbenVxeGrid({
label: $t('ui.actionTitle.create', ['告警配置']), label: $t('ui.actionTitle.create', ['告警配置']),
type: 'primary', type: 'primary',
icon: ACTION_ICON.ADD, icon: ACTION_ICON.ADD,
auth: ['iot:alert-config:create'],
onClick: handleCreate, onClick: handleCreate,
}, },
]" ]"
/> />
</template> </template>
<!-- TODO @AI可以在 data 里渲染么应该 antd 里有例子的 -->
<!-- 告警级别列 --> <!-- 告警级别列 -->
<template #level="{ row }"> <template #level="{ row }">
<Tag :color="getLevelColor(row.level)"> <Tag>
{{ getLevelText(row.level) }} {{ getDictLabel(DICT_TYPE.IOT_ALERT_LEVEL, row.level) }}
</Tag> </Tag>
</template> </template>
<!-- TODO @AI可以在 data 里渲染么应该 antd 里有例子的 -->
<!-- 关联场景联动规则列 --> <!-- 关联场景联动规则列 -->
<template #sceneRules="{ row }"> <template #sceneRules="{ row }">
<span>{{ row.sceneRuleIds?.length || 0 }} </span> <span>{{ row.sceneRuleIds?.length || 0 }} </span>
</template> </template>
<!-- TODO @AI可以在 data 里渲染么应该 antd 里有例子的 -->
<!-- 接收类型列 --> <!-- 接收类型列 -->
<template #receiveTypes="{ row }"> <template #receiveTypes="{ row }">
<Tag <Tag
@ -155,10 +122,9 @@ const [Grid, gridApi] = useVbenVxeGrid({
:key="index" :key="index"
class="mr-1" class="mr-1"
> >
{{ getReceiveTypeText(type) }} {{ getDictLabel(DICT_TYPE.IOT_ALERT_RECEIVE_TYPE, type) }}
</Tag> </Tag>
</template> </template>
<!-- 操作列 --> <!-- 操作列 -->
<template #actions="{ row }"> <template #actions="{ row }">
<TableAction <TableAction
@ -167,6 +133,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
label: $t('common.edit'), label: $t('common.edit'),
type: 'link', type: 'link',
icon: ACTION_ICON.EDIT, icon: ACTION_ICON.EDIT,
auth: ['iot:alert-config:update'],
onClick: handleEdit.bind(null, row), onClick: handleEdit.bind(null, row),
}, },
{ {
@ -174,6 +141,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
type: 'link', type: 'link',
danger: true, danger: true,
icon: ACTION_ICON.DELETE, icon: ACTION_ICON.DELETE,
auth: ['iot:alert-config:delete'],
popConfirm: { popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.name]), title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: handleDelete.bind(null, row), confirm: handleDelete.bind(null, row),

View File

@ -1,5 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { AlertConfigApi } from '#/api/iot/alert/config'; // TODO @AI form.vue system user form
// TODO @AI system user form
import type { AlertConfig } from '#/api/iot/alert/config';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
@ -20,7 +22,7 @@ import { useFormSchema } from '../config/data';
defineOptions({ name: 'IoTAlertConfigForm' }); defineOptions({ name: 'IoTAlertConfigForm' });
const emit = defineEmits(['success']); const emit = defineEmits(['success']);
const formData = ref<AlertConfigApi.AlertConfig>(); const formData = ref<AlertConfig>();
const getTitle = computed(() => { const getTitle = computed(() => {
return formData.value?.id return formData.value?.id
? $t('ui.actionTitle.edit', ['告警配置']) ? $t('ui.actionTitle.edit', ['告警配置'])
@ -47,7 +49,7 @@ const [Modal, modalApi] = useVbenModal({
} }
modalApi.lock(); modalApi.lock();
// //
const data = (await formApi.getValues()) as AlertConfigApi.AlertConfig; const data = (await formApi.getValues()) as AlertConfig;
try { try {
await (formData.value?.id await (formData.value?.id
? updateAlertConfig(data) ? updateAlertConfig(data)
@ -66,9 +68,10 @@ const [Modal, modalApi] = useVbenModal({
return; return;
} }
// //
const data = modalApi.getData<AlertConfigApi.AlertConfig>(); const data = modalApi.getData<AlertConfig>();
if (!data || !data.id) { if (!data || !data.id) {
// //
// TODO @AIstatus data.ts
await formApi.setValues({ await formApi.setValues({
status: 0, status: 0,
sceneRuleIds: [], sceneRuleIds: [],

View File

@ -1,110 +1,57 @@
<script setup lang="ts"> <script setup lang="ts">
// TODO @AI antd 1 modules2process
import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { AlertRecord } from '#/api/iot/alert/record'; import type { AlertRecord } from '#/api/iot/alert/record';
import { h, onMounted, ref } from 'vue'; import { h, ref } from 'vue';
import { Page } from '@vben/common-ui'; import { Page } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import { getDictLabel } from '@vben/hooks';
import { IconifyIcon } from '@vben/icons'; import { IconifyIcon } from '@vben/icons';
import { Button, message, Modal, Popover, Tag } from 'ant-design-vue'; import { Button, Input, message, Modal, Popover, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getAlertRecordPage, processAlertRecord } from '#/api/iot/alert/record'; import { getAlertRecordPage, processAlertRecord } from '#/api/iot/alert/record';
import { getSimpleDeviceList } from '#/api/iot/device/device';
import { getSimpleProductList } from '#/api/iot/product/product';
import { useGridColumns, useGridFormSchema } from './data'; import { useGridColumns, useGridFormSchema } from './data';
defineOptions({ name: 'IoTAlertRecord' }); defineOptions({ name: 'IoTAlertRecord' });
const productList = ref<any[]>([]);
const deviceList = ref<any[]>([]);
/** 刷新表格 */ /** 刷新表格 */
function handleRefresh() { function handleRefresh() {
gridApi.query(); gridApi.query();
} }
// /** 处理告警记录 */
async function loadData() { function handleProcess(row: AlertRecord) {
productList.value = await getSimpleProductList(); const processRemark = ref('');
deviceList.value = await getSimpleDeviceList();
}
//
function getLevelText(level?: number) {
const levelMap: Record<number, string> = {
1: '提示',
2: '一般',
3: '警告',
4: '严重',
5: '紧急',
};
return level ? levelMap[level] || `级别${level}` : '-';
}
//
function getLevelColor(level?: number) {
const colorMap: Record<number, string> = {
1: 'blue',
2: 'green',
3: 'orange',
4: 'red',
5: 'purple',
};
return level ? colorMap[level] || 'default' : 'default';
}
//
function getProductName(productId?: number) {
if (!productId) return '-';
const product = productList.value.find((p: any) => p.id === productId);
return product?.name || '加载中...';
}
//
function getDeviceName(deviceId?: number) {
if (!deviceId) return '-';
const device = deviceList.value.find((d: any) => d.id === deviceId);
return device?.deviceName || '加载中...';
}
//
async function handleProcess(row: AlertRecord) {
Modal.confirm({ Modal.confirm({
title: '处理告警记录', title: '处理告警记录',
content: h('div', [ content: () =>
h('p', '请输入处理原因:'), h('div', { class: 'space-y-2' }, [
h('textarea', { h('p', '请输入处理原因:'),
id: 'processRemark', h(Input.TextArea, {
class: 'ant-input', 'value': processRemark.value,
rows: 3, 'onUpdate:value': (val: string) => (processRemark.value = val),
placeholder: '请输入处理原因', 'rows': 3,
}), 'placeholder': '请输入处理原因',
]), }),
]),
async onOk() { async onOk() {
const textarea = document.querySelector( if (!processRemark.value) {
'#processRemark',
) as HTMLTextAreaElement;
const processRemark = textarea?.value || '';
if (!processRemark) {
message.warning('请输入处理原因'); message.warning('请输入处理原因');
throw new Error('请输入处理原因'); throw new Error('请输入处理原因');
} }
const hideLoading = message.loading({ const hideLoading = message.loading({
content: '正在处理...', content: '正在处理...',
duration: 0, duration: 0,
}); });
try { try {
await processAlertRecord(row.id as number, processRemark); await processAlertRecord(row.id as number, processRemark.value);
message.success('处理成功'); message.success('处理成功');
handleRefresh(); handleRefresh();
} catch (error) {
console.error('处理失败:', error);
throw error;
} finally { } finally {
hideLoading(); hideLoading();
} }
@ -112,8 +59,12 @@ async function handleProcess(row: AlertRecord) {
}); });
} }
// /** 查看告警记录详情 */
function handleView(row: AlertRecord) { function handleView(row: AlertRecord) {
const deviceMessageText =
row.deviceMessage && typeof row.deviceMessage === 'object'
? JSON.stringify(row.deviceMessage, null, 2)
: row.deviceMessage || '-';
Modal.info({ Modal.info({
title: '告警记录详情', title: '告警记录详情',
width: 600, width: 600,
@ -124,14 +75,17 @@ function handleView(row: AlertRecord) {
]), ]),
h('div', [ h('div', [
h('span', { class: 'font-semibold' }, '告警级别:'), h('span', { class: 'font-semibold' }, '告警级别:'),
h('span', getLevelText(row.configLevel)), h(
'span',
getDictLabel(DICT_TYPE.IOT_ALERT_LEVEL, row.configLevel) || '-',
),
]), ]),
h('div', [ h('div', [
h('span', { class: 'font-semibold' }, '设备消息:'), h('span', { class: 'font-semibold' }, '设备消息:'),
h( h(
'pre', 'pre',
{ class: 'mt-1 text-xs bg-gray-50 p-2 rounded' }, { class: 'mt-1 text-xs bg-gray-50 p-2 rounded' },
row.deviceMessage || '-', deviceMessageText,
), ),
]), ]),
h('div', [ h('div', [
@ -181,32 +135,29 @@ const [Grid, gridApi] = useVbenVxeGrid({
} as VxeTableGridOptions<AlertRecord>, } as VxeTableGridOptions<AlertRecord>,
}); });
onMounted(() => { /** 把设备消息序列化成可读字符串 */
loadData(); function stringifyDeviceMessage(deviceMessage: any) {
}); if (!deviceMessage) {
return '';
}
return typeof deviceMessage === 'object'
? JSON.stringify(deviceMessage, null, 2)
: String(deviceMessage);
}
</script> </script>
<template> <template>
<Page auto-content-height> <Page auto-content-height>
<Grid table-title=""> <Grid table-title="">
<!-- 告警级别列 --> <!-- 告警级别列 -->
<!-- TODO @AI可以在 data 里渲染么应该 antd 里有例子的 -->
<template #configLevel="{ row }"> <template #configLevel="{ row }">
<Tag :color="getLevelColor(row.configLevel)"> <Tag>
{{ getLevelText(row.configLevel) }} {{ getDictLabel(DICT_TYPE.IOT_ALERT_LEVEL, row.configLevel) }}
</Tag> </Tag>
</template> </template>
<!-- 产品名称列 -->
<template #product="{ row }">
<span>{{ getProductName(row.productId) }}</span>
</template>
<!-- 设备名称列 -->
<template #device="{ row }">
<span>{{ getDeviceName(row.deviceId) }}</span>
</template>
<!-- 设备消息列 --> <!-- 设备消息列 -->
<!-- TODO @AI可以在 data 里渲染么应该 antd 里有例子的 -->
<template #deviceMessage="{ row }"> <template #deviceMessage="{ row }">
<Popover <Popover
v-if="row.deviceMessage" v-if="row.deviceMessage"
@ -215,7 +166,7 @@ onMounted(() => {
:overlay-style="{ maxWidth: '600px' }" :overlay-style="{ maxWidth: '600px' }"
> >
<template #content> <template #content>
<pre class="text-xs">{{ row.deviceMessage }}</pre> <pre class="text-xs">{{ stringifyDeviceMessage(row.deviceMessage) }}</pre>
</template> </template>
<Button size="small" type="link"> <Button size="small" type="link">
<IconifyIcon icon="ant-design:eye-outlined" class="mr-1" /> <IconifyIcon icon="ant-design:eye-outlined" class="mr-1" />
@ -224,7 +175,6 @@ onMounted(() => {
</Popover> </Popover>
<span v-else class="text-gray-400">-</span> <span v-else class="text-gray-400">-</span>
</template> </template>
<!-- 操作列 --> <!-- 操作列 -->
<template #actions="{ row }"> <template #actions="{ row }">
<TableAction <TableAction
@ -233,6 +183,7 @@ onMounted(() => {
label: '处理', label: '处理',
type: 'link', type: 'link',
icon: ACTION_ICON.EDIT, icon: ACTION_ICON.EDIT,
auth: ['iot:alert-record:process'],
onClick: handleProcess.bind(null, row), onClick: handleProcess.bind(null, row),
ifShow: !row.processStatus, ifShow: !row.processStatus,
}, },
@ -240,6 +191,7 @@ onMounted(() => {
label: '查看', label: '查看',
type: 'link', type: 'link',
icon: ACTION_ICON.VIEW, icon: ACTION_ICON.VIEW,
auth: ['iot:alert-record:query'],
onClick: handleView.bind(null, row), onClick: handleView.bind(null, row),
ifShow: row.processStatus, ifShow: row.processStatus,
}, },