admin-vben/apps/web-antd/src/views/iot/rule/scene/index.vue

400 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { RuleSceneApi } from '#/api/iot/rule/scene';
import { ref } from 'vue';
import { Page, useVbenDrawer } from '@vben/common-ui';
import {
CommonStatusEnum,
DICT_TYPE,
getActionTypeLabel,
getTriggerTypeLabel,
IotRuleSceneTriggerTypeEnum,
} from '@vben/constants';
import { IconifyIcon } from '@vben/icons';
import { CronUtils, formatDateTime } from '@vben/utils';
import { Card, Col, message, Row, Tag, Tooltip } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteSceneRule,
getSceneRulePage,
updateSceneRuleStatus,
} from '#/api/iot/rule/scene';
import { DictTag } from '#/components/dict-tag';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormDrawer, formDrawerApi] = useVbenDrawer({
connectedComponent: Form,
destroyOnClose: true,
});
const statistics = ref({
total: 0,
enabled: 0,
disabled: 0,
timerRules: 0,
}); // 统计数据
/** 刷新表格 */
function handleRefresh() {
gridApi.query();
}
/** 创建场景规则 */
function handleCreate() {
formDrawerApi.setData(null).open();
}
/** 编辑场景规则 */
function handleEdit(row: RuleSceneApi.SceneRule) {
formDrawerApi.setData(row).open();
}
/** 启用/停用场景规则 */
async function handleToggleStatus(row: RuleSceneApi.SceneRule) {
const newStatus =
row.status === CommonStatusEnum.ENABLE
? CommonStatusEnum.DISABLE
: CommonStatusEnum.ENABLE;
const hideLoading = message.loading({
content:
newStatus === CommonStatusEnum.ENABLE ? '正在启用...' : '正在停用...',
duration: 0,
});
try {
await updateSceneRuleStatus(row.id as number, newStatus);
message.success({
content: newStatus === CommonStatusEnum.ENABLE ? '启用成功' : '停用成功',
});
handleRefresh();
} finally {
hideLoading();
}
}
/** 删除场景规则 */
async function handleDelete(row: RuleSceneApi.SceneRule) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
});
try {
await deleteSceneRule(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
});
handleRefresh();
} finally {
hideLoading();
}
}
/** 判断规则是否包含定时触发器 */
function hasTimerTrigger(row: RuleSceneApi.SceneRule): boolean {
return (
row.triggers?.some(
(trigger) => trigger.type === IotRuleSceneTriggerTypeEnum.TIMER,
) || false
);
}
/** 触发条件摘要文本(拼接所有触发器) */
function getTriggerSummary(row: RuleSceneApi.SceneRule): string {
if (!row.triggers?.length) return '无触发器';
return row.triggers
.map((trigger) => {
const type = trigger.type ?? 0;
let description = getTriggerTypeLabel(type);
if (
(type === IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST ||
type === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST ||
type === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE) &&
trigger.identifier
) {
description += ` (${trigger.identifier})`;
} else if (type === IotRuleSceneTriggerTypeEnum.TIMER) {
description = `${getTriggerTypeLabel(type)} (${CronUtils.format(trigger.cronExpression || '')})`;
}
if (trigger.deviceId) {
description += ` [设备 ID: ${trigger.deviceId}]`;
} else if (trigger.productId) {
description += ` [产品 ID: ${trigger.productId}]`;
}
return description;
})
.join(', ');
}
/** 执行动作摘要文本(拼接所有动作) */
function getActionSummary(row: RuleSceneApi.SceneRule): string {
if (!row.actions?.length) return '无执行器';
return row.actions
.map((action) => {
let description = getActionTypeLabel(action.type ?? 0);
if (action.deviceId) {
description += ` [设备 ID: ${action.deviceId}]`;
} else if (action.productId) {
description += ` [产品 ID: ${action.productId}]`;
}
if (action.alertConfigId) {
description += ` [告警配置 ID: ${action.alertConfigId}]`;
}
return description;
})
.join(', ');
}
/** 取定时触发器的 CRON 频率描述 */
function getCronFrequency(row: RuleSceneApi.SceneRule): string {
const timerTrigger = row.triggers?.find(
(trigger) => trigger.type === IotRuleSceneTriggerTypeEnum.TIMER,
);
return timerTrigger?.cronExpression
? CronUtils.getFrequencyDescription(timerTrigger.cronExpression)
: '';
}
/** 取定时触发器原始 CRON 表达式 */
function getCronExpression(row: RuleSceneApi.SceneRule): string {
const timerTrigger = row.triggers?.find(
(trigger) => trigger.type === IotRuleSceneTriggerTypeEnum.TIMER,
);
return timerTrigger?.cronExpression || '';
}
/** 取定时触发器下次执行时间 */
function getNextExecutionTime(row: RuleSceneApi.SceneRule): Date | null {
const timerTrigger = row.triggers?.find(
(trigger) => trigger.type === IotRuleSceneTriggerTypeEnum.TIMER,
);
return timerTrigger?.cronExpression
? CronUtils.getNextExecutionTime(timerTrigger.cronExpression)
: null;
}
/** 刷新规则统计卡片数据 */
function updateStatistics(rows: RuleSceneApi.SceneRule[], total?: number) {
statistics.value = {
total: total ?? rows.length,
enabled: rows.filter((item) => item.status === CommonStatusEnum.ENABLE)
.length,
disabled: rows.filter((item) => item.status === CommonStatusEnum.DISABLE)
.length,
timerRules: rows.filter((item) => hasTimerTrigger(item)).length,
};
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
const result = await getSceneRulePage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
updateStatistics(result.list || [], result.total);
return result;
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
search: true,
},
} as VxeTableGridOptions<RuleSceneApi.SceneRule>,
});
</script>
<template>
<Page auto-content-height>
<FormDrawer @success="handleRefresh" />
<!-- 统计卡片 -->
<Row :gutter="16" class="mb-4">
<Col :span="6">
<Card :body-style="{ padding: '12px 16px' }">
<div class="flex items-center">
<div
class="w-10 h-10 rounded-lg flex items-center justify-center text-xl text-white mr-3 bg-gradient-to-br from-indigo-500 to-purple-600"
>
<IconifyIcon icon="ant-design:file-text-outlined" />
</div>
<div class="leading-tight">
<div class="text-xl font-semibold">
{{ statistics.total }}
</div>
<div class="text-xs text-muted-foreground">总规则数</div>
</div>
</div>
</Card>
</Col>
<Col :span="6">
<Card :body-style="{ padding: '12px 16px' }">
<div class="flex items-center">
<div
class="w-10 h-10 rounded-lg flex items-center justify-center text-xl text-white mr-3 bg-gradient-to-br from-pink-400 to-red-500"
>
<IconifyIcon icon="ant-design:check-outlined" />
</div>
<div class="leading-tight">
<div class="text-xl font-semibold">
{{ statistics.enabled }}
</div>
<div class="text-xs text-muted-foreground">启用规则</div>
</div>
</div>
</Card>
</Col>
<Col :span="6">
<Card :body-style="{ padding: '12px 16px' }">
<div class="flex items-center">
<div
class="w-10 h-10 rounded-lg flex items-center justify-center text-xl text-white mr-3 bg-gradient-to-br from-cyan-400 to-blue-500"
>
<IconifyIcon icon="ant-design:close-outlined" />
</div>
<div class="leading-tight">
<div class="text-xl font-semibold">
{{ statistics.disabled }}
</div>
<div class="text-xs text-muted-foreground">禁用规则</div>
</div>
</div>
</Card>
</Col>
<Col :span="6">
<Card :body-style="{ padding: '12px 16px' }">
<div class="flex items-center">
<div
class="w-10 h-10 rounded-lg flex items-center justify-center text-xl text-white mr-3 bg-gradient-to-br from-green-400 to-teal-400"
>
<IconifyIcon icon="lucide:timer" />
</div>
<div class="leading-tight">
<div class="text-xl font-semibold">
{{ statistics.timerRules }}
</div>
<div class="text-xs text-muted-foreground">定时规则</div>
</div>
</div>
</Card>
</Col>
</Row>
<Grid table-title="">
<template #toolbar-tools>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['场景规则']),
type: 'primary',
icon: ACTION_ICON.ADD,
onClick: handleCreate,
},
]"
/>
</template>
<!-- 规则名称列名称 + 状态 tag inline + 描述 -->
<template #name="{ row }">
<div class="flex items-center gap-2">
<span class="font-medium text-foreground">{{ row.name }}</span>
<DictTag :type="DICT_TYPE.COMMON_STATUS" :value="row.status" />
</div>
<Tooltip
v-if="row.description"
:title="row.description"
placement="top"
>
<div
class="mt-1 max-w-[200px] truncate text-xs text-muted-foreground"
>
{{ row.description }}
</div>
</Tooltip>
</template>
<!-- 触发条件列:单 tag 汇总 + 定时触发额外信息 -->
<template #triggers="{ row }">
<div class="space-y-1">
<Tag color="processing" class="m-0">{{ getTriggerSummary(row) }}</Tag>
<Tooltip
v-if="hasTimerTrigger(row)"
:title="getCronExpression(row)"
placement="top"
>
<div class="text-xs text-muted-foreground">
<IconifyIcon icon="lucide:clock" class="mr-1 inline" />
{{ getCronFrequency(row) }}
<template v-if="getNextExecutionTime(row)">
· 下次 {{ formatDateTime(getNextExecutionTime(row) as Date) }}
</template>
</div>
</Tooltip>
</div>
</template>
<!-- 执行动作列:单 tag 汇总 -->
<template #actionsCol="{ row }">
<Tag color="success" class="m-0">{{ getActionSummary(row) }}</Tag>
</template>
<!-- 最近触发列 -->
<template #lastTriggerTime="{ row }">
<span v-if="row.lastTriggerTime">
{{ formatDateTime(row.lastTriggerTime) }}
</span>
<span v-else class="text-muted-foreground">未触发</span>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: row.status === CommonStatusEnum.ENABLE ? '停用' : '启用',
type: 'link',
icon:
row.status === CommonStatusEnum.ENABLE
? 'ant-design:stop-outlined'
: 'ant-design:check-circle-outlined',
popConfirm: {
title: `确认${row.status === CommonStatusEnum.ENABLE ? '停用' : '启用'}场景规则「${row.name}」吗?`,
confirm: handleToggleStatus.bind(null, row),
},
},
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Page>
</template>