feat: 优化 IoT 告警模板选择

- 后端 mail/sms/notify 模板 simple-list 仅返回启用模板精简字段
- 前端补充 mail/sms/notify 模板 simple-list API 封装
- vue3 与 vben antd/ele 在各自 system 模块封装模板选择组件
- IoT 告警配置按接收类型动态选择短信、邮件、站内信模板
- 补充前端 IotAlertReceiveTypeEnum,替换表单内裸常量
pull/351/MERGE
YunaiV 2026-05-30 22:06:02 +08:00
parent 3007539f0e
commit 7e62f9a5ef
23 changed files with 738 additions and 2 deletions

View File

@ -14,6 +14,9 @@ export namespace AlertConfigApi {
receiveUserIds?: number[];
receiveUserNames?: string[];
receiveTypes?: number[];
smsTemplateCode?: string;
mailTemplateCode?: string;
notifyTemplateCode?: string;
createTime?: Date;
}
}

View File

@ -17,6 +17,13 @@ export namespace SystemMailTemplateApi {
createTime: Date;
}
/** 邮件模版精简信息 */
export interface MailTemplateSimple {
id: number;
name: string;
code: string;
}
/** 邮件发送信息 */
export interface MailSendReqVO {
toMails: string[];
@ -35,6 +42,13 @@ export function getMailTemplatePage(params: PageParam) {
);
}
/** 查询邮件模版精简列表 */
export function getSimpleMailTemplateList() {
return requestClient.get<SystemMailTemplateApi.MailTemplateSimple[]>(
'/system/mail-template/simple-list',
);
}
/** 查询邮件模版详情 */
export function getMailTemplate(id: number) {
return requestClient.get<SystemMailTemplateApi.MailTemplate>(

View File

@ -16,6 +16,13 @@ export namespace SystemNotifyTemplateApi {
remark: string;
}
/** 站内信模板精简信息 */
export interface NotifyTemplateSimple {
id: number;
name: string;
code: string;
}
/** 发送站内信请求 */
export interface NotifySendReqVO {
userId: number;
@ -33,6 +40,13 @@ export function getNotifyTemplatePage(params: PageParam) {
);
}
/** 查询站内信模板精简列表 */
export function getSimpleNotifyTemplateList() {
return requestClient.get<SystemNotifyTemplateApi.NotifyTemplateSimple[]>(
'/system/notify-template/simple-list',
);
}
/** 查询站内信模板详情 */
export function getNotifyTemplate(id: number) {
return requestClient.get<SystemNotifyTemplateApi.NotifyTemplate>(

View File

@ -19,6 +19,13 @@ export namespace SystemSmsTemplateApi {
createTime?: Date;
}
/** 短信模板精简信息 */
export interface SmsTemplateSimple {
id: number;
name: string;
code: string;
}
/** 发送短信请求 */
export interface SmsSendReqVO {
mobile: string;
@ -35,6 +42,13 @@ export function getSmsTemplatePage(params: PageParam) {
);
}
/** 查询短信模板精简列表 */
export function getSimpleSmsTemplateList() {
return requestClient.get<SystemSmsTemplateApi.SmsTemplateSimple[]>(
'/system/sms-template/simple-list',
);
}
/** 查询短信模板详情 */
export function getSmsTemplate(id: number) {
return requestClient.get<SystemSmsTemplateApi.SmsTemplate>(

View File

@ -2,12 +2,25 @@ import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { AlertConfigApi } from '#/api/iot/alert/config';
import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
import { markRaw } from 'vue';
import {
CommonStatusEnum,
DICT_TYPE,
IotAlertReceiveTypeEnum,
} from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { getSimpleRuleSceneList } from '#/api/iot/rule/scene';
import { getSimpleUserList } from '#/api/system/user';
import { getRangePickerDefaultProps } from '#/utils';
import { MailTemplateSelect } from '#/views/system/mail/template/components';
import { NotifyTemplateSelect } from '#/views/system/notify/template/components';
import { SmsTemplateSelect } from '#/views/system/sms/template/components';
function hasReceiveType(values: Partial<Record<string, any>>, type: number) {
return Array.isArray(values.receiveTypes) && values.receiveTypes.includes(type);
}
/** 新增/修改告警配置的表单 */
export function useFormSchema(): VbenFormSchema[] {
@ -100,6 +113,60 @@ export function useFormSchema(): VbenFormSchema[] {
defaultValue: [],
rules: 'required',
},
{
fieldName: 'smsTemplateCode',
label: '短信模板',
component: markRaw(SmsTemplateSelect),
dependencies: {
triggerFields: ['receiveTypes'],
show: (values) => hasReceiveType(values, IotAlertReceiveTypeEnum.SMS),
trigger: async (values, formApi) => {
if (
!hasReceiveType(values, IotAlertReceiveTypeEnum.SMS) &&
values.smsTemplateCode
) {
await formApi.setFieldValue('smsTemplateCode', undefined);
}
},
},
rules: 'selectRequired',
},
{
fieldName: 'mailTemplateCode',
label: '邮件模板',
component: markRaw(MailTemplateSelect),
dependencies: {
triggerFields: ['receiveTypes'],
show: (values) => hasReceiveType(values, IotAlertReceiveTypeEnum.MAIL),
trigger: async (values, formApi) => {
if (
!hasReceiveType(values, IotAlertReceiveTypeEnum.MAIL) &&
values.mailTemplateCode
) {
await formApi.setFieldValue('mailTemplateCode', undefined);
}
},
},
rules: 'selectRequired',
},
{
fieldName: 'notifyTemplateCode',
label: '站内信模板',
component: markRaw(NotifyTemplateSelect),
dependencies: {
triggerFields: ['receiveTypes'],
show: (values) => hasReceiveType(values, IotAlertReceiveTypeEnum.NOTIFY),
trigger: async (values, formApi) => {
if (
!hasReceiveType(values, IotAlertReceiveTypeEnum.NOTIFY) &&
values.notifyTemplateCode
) {
await formApi.setFieldValue('notifyTemplateCode', undefined);
}
},
},
rules: 'selectRequired',
},
];
}

View File

@ -0,0 +1 @@
export { default as MailTemplateSelect } from './select.vue';

View File

@ -0,0 +1,82 @@
<script lang="ts" setup>
import type { SelectValue } from 'ant-design-vue/es/select';
import type { SystemMailTemplateApi } from '#/api/system/mail/template';
import { computed, onMounted, ref } from 'vue';
import { Select } from 'ant-design-vue';
import { getSimpleMailTemplateList } from '#/api/system/mail/template';
defineOptions({ name: 'MailTemplateSelect', inheritAttrs: false });
const props = withDefaults(
defineProps<{
allowClear?: boolean;
disabled?: boolean;
modelValue?: string;
placeholder?: string;
}>(),
{
allowClear: true,
disabled: false,
modelValue: undefined,
placeholder: '请选择邮件模板',
},
);
const emit = defineEmits<{
change: [template: SystemMailTemplateApi.MailTemplateSimple | undefined];
'update:modelValue': [value: string | undefined];
}>();
const loading = ref(false);
const templateList = ref<SystemMailTemplateApi.MailTemplateSimple[]>([]);
const options = computed(() =>
templateList.value.map((template) => ({
label: `${template.name}${template.code}`,
value: template.code,
})),
);
/** 选中变化 */
function handleChange(value: SelectValue) {
const templateCode = typeof value === 'string' ? value : undefined;
emit('update:modelValue', templateCode);
emit(
'change',
templateList.value.find((template) => template.code === templateCode),
);
}
/** 查询邮件模板精简列表 */
async function getList() {
try {
loading.value = true;
templateList.value = await getSimpleMailTemplateList();
} finally {
loading.value = false;
}
}
onMounted(() => {
getList();
});
</script>
<template>
<Select
v-bind="$attrs"
:allow-clear="allowClear"
:disabled="disabled"
:loading="loading"
:options="options"
:placeholder="placeholder"
:value="props.modelValue"
class="w-full"
option-filter-prop="label"
show-search
@change="handleChange"
/>
</template>

View File

@ -0,0 +1 @@
export { default as NotifyTemplateSelect } from './select.vue';

View File

@ -0,0 +1,82 @@
<script lang="ts" setup>
import type { SelectValue } from 'ant-design-vue/es/select';
import type { SystemNotifyTemplateApi } from '#/api/system/notify/template';
import { computed, onMounted, ref } from 'vue';
import { Select } from 'ant-design-vue';
import { getSimpleNotifyTemplateList } from '#/api/system/notify/template';
defineOptions({ name: 'NotifyTemplateSelect', inheritAttrs: false });
const props = withDefaults(
defineProps<{
allowClear?: boolean;
disabled?: boolean;
modelValue?: string;
placeholder?: string;
}>(),
{
allowClear: true,
disabled: false,
modelValue: undefined,
placeholder: '请选择站内信模板',
},
);
const emit = defineEmits<{
change: [template: SystemNotifyTemplateApi.NotifyTemplateSimple | undefined];
'update:modelValue': [value: string | undefined];
}>();
const loading = ref(false);
const templateList = ref<SystemNotifyTemplateApi.NotifyTemplateSimple[]>([]);
const options = computed(() =>
templateList.value.map((template) => ({
label: `${template.name}${template.code}`,
value: template.code,
})),
);
/** 选中变化 */
function handleChange(value: SelectValue) {
const templateCode = typeof value === 'string' ? value : undefined;
emit('update:modelValue', templateCode);
emit(
'change',
templateList.value.find((template) => template.code === templateCode),
);
}
/** 查询站内信模板精简列表 */
async function getList() {
try {
loading.value = true;
templateList.value = await getSimpleNotifyTemplateList();
} finally {
loading.value = false;
}
}
onMounted(() => {
getList();
});
</script>
<template>
<Select
v-bind="$attrs"
:allow-clear="allowClear"
:disabled="disabled"
:loading="loading"
:options="options"
:placeholder="placeholder"
:value="props.modelValue"
class="w-full"
option-filter-prop="label"
show-search
@change="handleChange"
/>
</template>

View File

@ -0,0 +1 @@
export { default as SmsTemplateSelect } from './select.vue';

View File

@ -0,0 +1,82 @@
<script lang="ts" setup>
import type { SelectValue } from 'ant-design-vue/es/select';
import type { SystemSmsTemplateApi } from '#/api/system/sms/template';
import { computed, onMounted, ref } from 'vue';
import { Select } from 'ant-design-vue';
import { getSimpleSmsTemplateList } from '#/api/system/sms/template';
defineOptions({ name: 'SmsTemplateSelect', inheritAttrs: false });
const props = withDefaults(
defineProps<{
allowClear?: boolean;
disabled?: boolean;
modelValue?: string;
placeholder?: string;
}>(),
{
allowClear: true,
disabled: false,
modelValue: undefined,
placeholder: '请选择短信模板',
},
);
const emit = defineEmits<{
change: [template: SystemSmsTemplateApi.SmsTemplateSimple | undefined];
'update:modelValue': [value: string | undefined];
}>();
const loading = ref(false);
const templateList = ref<SystemSmsTemplateApi.SmsTemplateSimple[]>([]);
const options = computed(() =>
templateList.value.map((template) => ({
label: `${template.name}${template.code}`,
value: template.code,
})),
);
/** 选中变化 */
function handleChange(value: SelectValue) {
const templateCode = typeof value === 'string' ? value : undefined;
emit('update:modelValue', templateCode);
emit(
'change',
templateList.value.find((template) => template.code === templateCode),
);
}
/** 查询短信模板精简列表 */
async function getList() {
try {
loading.value = true;
templateList.value = await getSimpleSmsTemplateList();
} finally {
loading.value = false;
}
}
onMounted(() => {
getList();
});
</script>
<template>
<Select
v-bind="$attrs"
:allow-clear="allowClear"
:disabled="disabled"
:loading="loading"
:options="options"
:placeholder="placeholder"
:value="props.modelValue"
class="w-full"
option-filter-prop="label"
show-search
@change="handleChange"
/>
</template>

View File

@ -14,6 +14,9 @@ export namespace AlertConfigApi {
receiveUserIds?: number[];
receiveUserNames?: string[];
receiveTypes?: number[];
smsTemplateCode?: string;
mailTemplateCode?: string;
notifyTemplateCode?: string;
createTime?: Date;
}
}

View File

@ -17,6 +17,13 @@ export namespace SystemMailTemplateApi {
createTime: Date;
}
/** 邮件模版精简信息 */
export interface MailTemplateSimple {
id: number;
name: string;
code: string;
}
/** 邮件发送信息 */
export interface MailSendReqVO {
toMails: string[];
@ -35,6 +42,13 @@ export function getMailTemplatePage(params: PageParam) {
);
}
/** 查询邮件模版精简列表 */
export function getSimpleMailTemplateList() {
return requestClient.get<SystemMailTemplateApi.MailTemplateSimple[]>(
'/system/mail-template/simple-list',
);
}
/** 查询邮件模版详情 */
export function getMailTemplate(id: number) {
return requestClient.get<SystemMailTemplateApi.MailTemplate>(

View File

@ -16,6 +16,13 @@ export namespace SystemNotifyTemplateApi {
remark: string;
}
/** 站内信模板精简信息 */
export interface NotifyTemplateSimple {
id: number;
name: string;
code: string;
}
/** 发送站内信请求 */
export interface NotifySendReqVO {
userId: number;
@ -33,6 +40,13 @@ export function getNotifyTemplatePage(params: PageParam) {
);
}
/** 查询站内信模板精简列表 */
export function getSimpleNotifyTemplateList() {
return requestClient.get<SystemNotifyTemplateApi.NotifyTemplateSimple[]>(
'/system/notify-template/simple-list',
);
}
/** 查询站内信模板详情 */
export function getNotifyTemplate(id: number) {
return requestClient.get<SystemNotifyTemplateApi.NotifyTemplate>(

View File

@ -19,6 +19,13 @@ export namespace SystemSmsTemplateApi {
createTime?: Date;
}
/** 短信模板精简信息 */
export interface SmsTemplateSimple {
id: number;
name: string;
code: string;
}
/** 发送短信请求 */
export interface SmsSendReqVO {
mobile: string;
@ -35,6 +42,13 @@ export function getSmsTemplatePage(params: PageParam) {
);
}
/** 查询短信模板精简列表 */
export function getSimpleSmsTemplateList() {
return requestClient.get<SystemSmsTemplateApi.SmsTemplateSimple[]>(
'/system/sms-template/simple-list',
);
}
/** 查询短信模板详情 */
export function getSmsTemplate(id: number) {
return requestClient.get<SystemSmsTemplateApi.SmsTemplate>(

View File

@ -2,12 +2,25 @@ import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { AlertConfigApi } from '#/api/iot/alert/config';
import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
import { markRaw } from 'vue';
import {
CommonStatusEnum,
DICT_TYPE,
IotAlertReceiveTypeEnum,
} from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { getSimpleRuleSceneList } from '#/api/iot/rule/scene';
import { getSimpleUserList } from '#/api/system/user';
import { getRangePickerDefaultProps } from '#/utils';
import { MailTemplateSelect } from '#/views/system/mail/template/components';
import { NotifyTemplateSelect } from '#/views/system/notify/template/components';
import { SmsTemplateSelect } from '#/views/system/sms/template/components';
function hasReceiveType(values: Partial<Record<string, any>>, type: number) {
return Array.isArray(values.receiveTypes) && values.receiveTypes.includes(type);
}
/** 新增/修改告警配置的表单 */
export function useFormSchema(): VbenFormSchema[] {
@ -98,6 +111,60 @@ export function useFormSchema(): VbenFormSchema[] {
defaultValue: [],
rules: 'required',
},
{
fieldName: 'smsTemplateCode',
label: '短信模板',
component: markRaw(SmsTemplateSelect),
dependencies: {
triggerFields: ['receiveTypes'],
show: (values) => hasReceiveType(values, IotAlertReceiveTypeEnum.SMS),
trigger: async (values, formApi) => {
if (
!hasReceiveType(values, IotAlertReceiveTypeEnum.SMS) &&
values.smsTemplateCode
) {
formApi.setFieldValue('smsTemplateCode', undefined);
}
},
},
rules: 'selectRequired',
},
{
fieldName: 'mailTemplateCode',
label: '邮件模板',
component: markRaw(MailTemplateSelect),
dependencies: {
triggerFields: ['receiveTypes'],
show: (values) => hasReceiveType(values, IotAlertReceiveTypeEnum.MAIL),
trigger: async (values, formApi) => {
if (
!hasReceiveType(values, IotAlertReceiveTypeEnum.MAIL) &&
values.mailTemplateCode
) {
formApi.setFieldValue('mailTemplateCode', undefined);
}
},
},
rules: 'selectRequired',
},
{
fieldName: 'notifyTemplateCode',
label: '站内信模板',
component: markRaw(NotifyTemplateSelect),
dependencies: {
triggerFields: ['receiveTypes'],
show: (values) => hasReceiveType(values, IotAlertReceiveTypeEnum.NOTIFY),
trigger: async (values, formApi) => {
if (
!hasReceiveType(values, IotAlertReceiveTypeEnum.NOTIFY) &&
values.notifyTemplateCode
) {
formApi.setFieldValue('notifyTemplateCode', undefined);
}
},
},
rules: 'selectRequired',
},
];
}

View File

@ -0,0 +1 @@
export { default as MailTemplateSelect } from './select.vue';

View File

@ -0,0 +1,84 @@
<script lang="ts" setup>
import type { SystemMailTemplateApi } from '#/api/system/mail/template';
import { computed, onMounted, ref } from 'vue';
import { ElOption, ElSelect } from 'element-plus';
import { getSimpleMailTemplateList } from '#/api/system/mail/template';
defineOptions({ name: 'MailTemplateSelect', inheritAttrs: false });
const props = withDefaults(
defineProps<{
clearable?: boolean;
disabled?: boolean;
modelValue?: string;
placeholder?: string;
}>(),
{
clearable: true,
disabled: false,
modelValue: undefined,
placeholder: '请选择邮件模板',
},
);
const emit = defineEmits<{
change: [template: SystemMailTemplateApi.MailTemplateSimple | undefined];
'update:modelValue': [value: string | undefined];
}>();
const loading = ref(false);
const templateList = ref<SystemMailTemplateApi.MailTemplateSimple[]>([]);
const selectValue = computed({
get: () => props.modelValue,
set: (value: string | undefined) => {
emit('update:modelValue', value || undefined);
},
});
/** 选中变化 */
function handleChange(value: string | undefined) {
emit(
'change',
templateList.value.find((template) => template.code === value),
);
}
/** 查询邮件模板精简列表 */
async function getList() {
try {
loading.value = true;
templateList.value = await getSimpleMailTemplateList();
} finally {
loading.value = false;
}
}
onMounted(() => {
getList();
});
</script>
<template>
<ElSelect
v-bind="$attrs"
v-model="selectValue"
:clearable="clearable"
:disabled="disabled"
:loading="loading"
:placeholder="placeholder"
class="w-full"
filterable
@change="handleChange"
>
<ElOption
v-for="template in templateList"
:key="template.id"
:label="`${template.name}${template.code}`"
:value="template.code"
/>
</ElSelect>
</template>

View File

@ -0,0 +1 @@
export { default as NotifyTemplateSelect } from './select.vue';

View File

@ -0,0 +1,84 @@
<script lang="ts" setup>
import type { SystemNotifyTemplateApi } from '#/api/system/notify/template';
import { computed, onMounted, ref } from 'vue';
import { ElOption, ElSelect } from 'element-plus';
import { getSimpleNotifyTemplateList } from '#/api/system/notify/template';
defineOptions({ name: 'NotifyTemplateSelect', inheritAttrs: false });
const props = withDefaults(
defineProps<{
clearable?: boolean;
disabled?: boolean;
modelValue?: string;
placeholder?: string;
}>(),
{
clearable: true,
disabled: false,
modelValue: undefined,
placeholder: '请选择站内信模板',
},
);
const emit = defineEmits<{
change: [template: SystemNotifyTemplateApi.NotifyTemplateSimple | undefined];
'update:modelValue': [value: string | undefined];
}>();
const loading = ref(false);
const templateList = ref<SystemNotifyTemplateApi.NotifyTemplateSimple[]>([]);
const selectValue = computed({
get: () => props.modelValue,
set: (value: string | undefined) => {
emit('update:modelValue', value || undefined);
},
});
/** 选中变化 */
function handleChange(value: string | undefined) {
emit(
'change',
templateList.value.find((template) => template.code === value),
);
}
/** 查询站内信模板精简列表 */
async function getList() {
try {
loading.value = true;
templateList.value = await getSimpleNotifyTemplateList();
} finally {
loading.value = false;
}
}
onMounted(() => {
getList();
});
</script>
<template>
<ElSelect
v-bind="$attrs"
v-model="selectValue"
:clearable="clearable"
:disabled="disabled"
:loading="loading"
:placeholder="placeholder"
class="w-full"
filterable
@change="handleChange"
>
<ElOption
v-for="template in templateList"
:key="template.id"
:label="`${template.name}${template.code}`"
:value="template.code"
/>
</ElSelect>
</template>

View File

@ -0,0 +1 @@
export { default as SmsTemplateSelect } from './select.vue';

View File

@ -0,0 +1,84 @@
<script lang="ts" setup>
import type { SystemSmsTemplateApi } from '#/api/system/sms/template';
import { computed, onMounted, ref } from 'vue';
import { ElOption, ElSelect } from 'element-plus';
import { getSimpleSmsTemplateList } from '#/api/system/sms/template';
defineOptions({ name: 'SmsTemplateSelect', inheritAttrs: false });
const props = withDefaults(
defineProps<{
clearable?: boolean;
disabled?: boolean;
modelValue?: string;
placeholder?: string;
}>(),
{
clearable: true,
disabled: false,
modelValue: undefined,
placeholder: '请选择短信模板',
},
);
const emit = defineEmits<{
change: [template: SystemSmsTemplateApi.SmsTemplateSimple | undefined];
'update:modelValue': [value: string | undefined];
}>();
const loading = ref(false);
const templateList = ref<SystemSmsTemplateApi.SmsTemplateSimple[]>([]);
const selectValue = computed({
get: () => props.modelValue,
set: (value: string | undefined) => {
emit('update:modelValue', value || undefined);
},
});
/** 选中变化 */
function handleChange(value: string | undefined) {
emit(
'change',
templateList.value.find((template) => template.code === value),
);
}
/** 查询短信模板精简列表 */
async function getList() {
try {
loading.value = true;
templateList.value = await getSimpleSmsTemplateList();
} finally {
loading.value = false;
}
}
onMounted(() => {
getList();
});
</script>
<template>
<ElSelect
v-bind="$attrs"
v-model="selectValue"
:clearable="clearable"
:disabled="disabled"
:loading="loading"
:placeholder="placeholder"
class="w-full"
filterable
@change="handleChange"
>
<ElOption
v-for="template in templateList"
:key="template.id"
:label="`${template.name}${template.code}`"
:value="template.code"
/>
</ElSelect>
</template>

View File

@ -85,6 +85,14 @@ export const CodecTypeEnum = {
ALINK: 'Alink', // 阿里云 Alink 协议
} as const;
// ========== IOT - 告警模块 ==========
/** IoT 告警接收方式枚举,与后端 IotAlertReceiveTypeEnum 保持一致 */
export const IotAlertReceiveTypeEnum = {
SMS: 1, // 短信
MAIL: 2, // 邮箱
NOTIFY: 3, // 站内信
} as const;
// ========== IOT - 物模型 ==========
/** IoT 产品物模型类型枚举类 */
export const IoTThingModelTypeEnum = {