commit
37c70daaaf
|
|
@ -10,6 +10,9 @@ export interface AlertConfig {
|
|||
sceneRuleIds: string // 关联的场景联动规则编号数组
|
||||
receiveUserIds: string // 接收的用户编号数组
|
||||
receiveTypes: string // 接收的类型数组
|
||||
smsTemplateCode?: string // 短信模板编号
|
||||
mailTemplateCode?: string // 邮件模板编号
|
||||
notifyTemplateCode?: string // 站内信模板编号
|
||||
}
|
||||
|
||||
// IoT 告警配置 API
|
||||
|
|
|
|||
|
|
@ -215,8 +215,8 @@ export const ThingModelFormRules = {
|
|||
identifier: [
|
||||
{ required: true, message: '标识符不能为空', trigger: 'blur' },
|
||||
{
|
||||
pattern: /^[a-zA-Z0-9_]{1,50}$/,
|
||||
message: '支持大小写字母、数字和下划线,不超过 50 个字符',
|
||||
pattern: /^[a-zA-Z][a-zA-Z0-9_]{0,31}$/,
|
||||
message: '支持大小写字母、数字和下划线,必须以字母开头,不超过 32 个字符',
|
||||
trigger: 'blur'
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export interface UserVO {
|
|||
loginIp: string
|
||||
mark: string
|
||||
mobile: string
|
||||
email: string | undefined
|
||||
name: string | undefined
|
||||
nickname: string | undefined
|
||||
registerIp: string
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ export interface ProAndonConfigVO {
|
|||
reason: string // 呼叫原因
|
||||
level: number // 级别
|
||||
handlerRoleId: number // 处置人角色编号
|
||||
handlerRoleName: string // 处置人角色名称
|
||||
handlerUserId: number // 处置人编号
|
||||
handlerUserNickname: string // 处置人昵称
|
||||
remark: string // 备注
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ export interface WmMiscIssueVO {
|
|||
id: number
|
||||
code: string
|
||||
name: string
|
||||
type: string
|
||||
type: number
|
||||
sourceDocType: string
|
||||
sourceDocId: number
|
||||
sourceDocCode: string
|
||||
|
|
|
|||
|
|
@ -19,6 +19,16 @@ export interface MailSendReqVO {
|
|||
templateParams: Map<String, Object>
|
||||
}
|
||||
|
||||
export interface MailTemplateSimpleVO {
|
||||
id: number
|
||||
name: string
|
||||
code: string
|
||||
}
|
||||
|
||||
// 查询邮件模版精简列表
|
||||
export const getSimpleMailTemplateList = async () => {
|
||||
return await request.get({ url: '/system/mail-template/simple-list' })
|
||||
}
|
||||
// 查询邮件模版列表
|
||||
export const getMailTemplatePage = async (params: PageParam) => {
|
||||
return await request.get({ url: '/system/mail-template/page', params })
|
||||
|
|
|
|||
|
|
@ -18,6 +18,17 @@ export interface NotifySendReqVO {
|
|||
templateParams: Map<String, Object>
|
||||
}
|
||||
|
||||
export interface NotifyTemplateSimpleVO {
|
||||
id: number
|
||||
name: string
|
||||
code: string
|
||||
}
|
||||
|
||||
// 查询站内信模板精简列表
|
||||
export const getSimpleNotifyTemplateList = async () => {
|
||||
return await request.get({ url: '/system/notify-template/simple-list' })
|
||||
}
|
||||
|
||||
// 查询站内信模板列表
|
||||
export const getNotifyTemplatePage = async (params: PageParam) => {
|
||||
return await request.get({ url: '/system/notify-template/page', params })
|
||||
|
|
@ -45,7 +56,10 @@ export const deleteNotifyTemplate = async (id: number) => {
|
|||
|
||||
// 批量删除站内信模板
|
||||
export const deleteNotifyTemplateList = async (ids: number[]) => {
|
||||
return await request.delete({ url: '/system/notify-template/delete-list', params: { ids: ids.join(',') } })
|
||||
return await request.delete({
|
||||
url: '/system/notify-template/delete-list',
|
||||
params: { ids: ids.join(',') }
|
||||
})
|
||||
}
|
||||
|
||||
// 发送站内信
|
||||
|
|
|
|||
|
|
@ -21,6 +21,16 @@ export interface SendSmsReqVO {
|
|||
templateParams: Map<String, Object>
|
||||
}
|
||||
|
||||
export interface SmsTemplateSimpleVO {
|
||||
id: number
|
||||
name: string
|
||||
code: string
|
||||
}
|
||||
|
||||
// 查询短信模板精简列表
|
||||
export const getSimpleSmsTemplateList = () => {
|
||||
return request.get({ url: '/system/sms-template/simple-list' })
|
||||
}
|
||||
// 查询短信模板列表
|
||||
export const getSmsTemplatePage = (params: PageParam) => {
|
||||
return request.get({ url: '/system/sms-template/page', params })
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
<!-- 数据字典 Select 选择器 -->
|
||||
<template>
|
||||
<el-select v-if="selectType === 'select'" class="w-1/1" v-bind="attrs">
|
||||
<el-select
|
||||
v-if="selectType === 'select'"
|
||||
v-model="selectedValue"
|
||||
class="w-1/1"
|
||||
v-bind="attrs"
|
||||
@change="handleChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="(dict, index) in getDictOptions"
|
||||
:key="index"
|
||||
|
|
@ -8,12 +14,24 @@
|
|||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-radio-group v-if="selectType === 'radio'" class="w-1/1" v-bind="attrs">
|
||||
<el-radio-group
|
||||
v-if="selectType === 'radio'"
|
||||
v-model="selectedValue"
|
||||
class="w-1/1"
|
||||
v-bind="attrs"
|
||||
@change="handleChange"
|
||||
>
|
||||
<el-radio v-for="(dict, index) in getDictOptions" :key="index" :value="dict.value">
|
||||
{{ dict.label }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
<el-checkbox-group v-if="selectType === 'checkbox'" class="w-1/1" v-bind="attrs">
|
||||
<el-checkbox-group
|
||||
v-if="selectType === 'checkbox'"
|
||||
v-model="selectedValue"
|
||||
class="w-1/1"
|
||||
v-bind="attrs"
|
||||
@change="handleChange"
|
||||
>
|
||||
<el-checkbox
|
||||
v-for="(dict, index) in getDictOptions"
|
||||
:key="index"
|
||||
|
|
@ -33,6 +51,7 @@ const attrs = useAttrs()
|
|||
// 接受父组件参数
|
||||
interface Props {
|
||||
dictType: string // 字典类型
|
||||
modelValue?: any // 选中值,由 form-create 默认 modelValue 绑定
|
||||
valueType?: 'str' | 'int' | 'bool' // 字典值类型
|
||||
selectType?: 'select' | 'radio' | 'checkbox' // 选择器类型,下拉框 select、多选框 checkbox、单选框 radio
|
||||
formCreateInject?: any
|
||||
|
|
@ -43,6 +62,20 @@ const props = withDefaults(defineProps<Props>(), {
|
|||
selectType: 'select'
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: any): void
|
||||
}>()
|
||||
|
||||
const selectedValue = ref<any>()
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
selectedValue.value = newValue
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// 获得字典配置
|
||||
const getDictOptions = computed(() => {
|
||||
switch (props.valueType) {
|
||||
|
|
@ -56,4 +89,8 @@ const getDictOptions = computed(() => {
|
|||
return []
|
||||
}
|
||||
})
|
||||
|
||||
const handleChange = (value: any) => {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,53 @@
|
|||
<script setup lang="ts">
|
||||
import * as ProcessInstanceApi from '@/api/bpm/processInstance'
|
||||
import * as AreaApi from '@/api/system/area'
|
||||
import * as DeptApi from '@/api/system/dept'
|
||||
import * as UserApi from '@/api/system/user'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { formatDate } from '@/utils/formatTime'
|
||||
import { DICT_TYPE, getDictLabel } from '@/utils/dict'
|
||||
import {
|
||||
DICT_TYPE,
|
||||
getBoolDictOptions,
|
||||
getDictLabel,
|
||||
getDictOptions,
|
||||
getIntDictOptions,
|
||||
getStrDictOptions
|
||||
} from '@/utils/dict'
|
||||
import { decodeFields } from '@/utils/formCreate'
|
||||
|
||||
interface FormFieldItem {
|
||||
html: string
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
interface FormFieldOption {
|
||||
label?: string
|
||||
value?: unknown
|
||||
}
|
||||
|
||||
type FormFieldRule = Record<string, unknown> & {
|
||||
field?: string
|
||||
options?: FormFieldOption[]
|
||||
props?: Record<string, unknown>
|
||||
title?: string
|
||||
type?: string
|
||||
}
|
||||
|
||||
type PrintableRecord = Record<string, unknown>
|
||||
|
||||
interface AreaNode {
|
||||
children?: AreaNode[]
|
||||
id?: number
|
||||
name: string
|
||||
}
|
||||
|
||||
interface PrintLookupMaps {
|
||||
areaMap: Map<string, string>
|
||||
deptMap: Map<string, string>
|
||||
userMap: Map<string, string>
|
||||
}
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const visible = ref(false)
|
||||
|
|
@ -13,15 +56,16 @@ const loading = ref(false)
|
|||
const printData = ref()
|
||||
const userName = computed(() => userStore.user.nickname ?? '')
|
||||
const printTime = ref(formatDate(new Date(), 'YYYY-MM-DD HH:mm'))
|
||||
const formFields = ref()
|
||||
const printDataMap = ref({})
|
||||
const formFields = ref<FormFieldItem[]>([])
|
||||
const printDataMap = ref<Record<string, string>>({})
|
||||
|
||||
const open = async (id: string) => {
|
||||
loading.value = true
|
||||
try {
|
||||
printData.value = await ProcessInstanceApi.getProcessInstancePrintData(id)
|
||||
printTime.value = formatDate(new Date(), 'YYYY-MM-DD HH:mm')
|
||||
initPrintDataMap()
|
||||
parseFormFields()
|
||||
await parseFormFields()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
|
@ -29,82 +73,347 @@ const open = async (id: string) => {
|
|||
}
|
||||
defineExpose({ open })
|
||||
|
||||
const parseFormFields = () => {
|
||||
const parseFormFields = async () => {
|
||||
if (!printData.value) return
|
||||
|
||||
const formFieldsObj = decodeFields(
|
||||
printData.value.processInstance.processDefinition?.formFields || []
|
||||
)
|
||||
const processVariables = printData.value.processInstance.formVariables
|
||||
let res: any = []
|
||||
) as FormFieldRule[]
|
||||
const processVariables = printData.value.processInstance.formVariables ?? {}
|
||||
const lookupMaps = await loadPrintLookupMaps(formFieldsObj)
|
||||
const res: FormFieldItem[] = []
|
||||
|
||||
for (const item of formFieldsObj) {
|
||||
const id = item['field']
|
||||
const name = item['title']
|
||||
const variable = processVariables[item['field']]
|
||||
let html = variable
|
||||
switch (item['type']) {
|
||||
case 'UploadImg': {
|
||||
let imgEl = document.createElement('img')
|
||||
imgEl.setAttribute('src', variable)
|
||||
imgEl.setAttribute('style', 'max-width: 600px;')
|
||||
html = imgEl.outerHTML
|
||||
break
|
||||
}
|
||||
case 'radio':
|
||||
case 'checkbox':
|
||||
case 'select': {
|
||||
const options = item['options'] || []
|
||||
const temp: any = []
|
||||
if (Array.isArray(variable)) {
|
||||
const labels = options.filter((o) => variable.includes(o.value)).map((o) => o.label)
|
||||
temp.push(...labels)
|
||||
} else {
|
||||
const opt = options.find((o) => o.value === variable)
|
||||
temp.push(opt.label)
|
||||
}
|
||||
html = temp.join(',')
|
||||
}
|
||||
// TODO 更多表单打印展示
|
||||
}
|
||||
printDataMap.value[item['field']] = html
|
||||
const fieldKey = String(item.field ?? '')
|
||||
const id = fieldKey
|
||||
const name = String(item.title ?? fieldKey)
|
||||
const variable = processVariables[fieldKey]
|
||||
const html = formatPrintField(item, variable, lookupMaps)
|
||||
|
||||
printDataMap.value[fieldKey] = html
|
||||
res.push({ id, name, html })
|
||||
}
|
||||
formFields.value = res
|
||||
}
|
||||
|
||||
const getRuleProp = (rule: FormFieldRule, key: string) => {
|
||||
return rule?.[key] ?? rule?.props?.[key]
|
||||
}
|
||||
|
||||
const isPrintableRecord = (value: unknown): value is PrintableRecord => {
|
||||
return typeof value === 'object' && value !== null
|
||||
}
|
||||
|
||||
const getRecordValue = (record: PrintableRecord, key: string) => {
|
||||
return record[key]
|
||||
}
|
||||
|
||||
const isNotEmptyString = (value: string) => value.length > 0
|
||||
|
||||
const isEmptyValue = (value: unknown) => value === undefined || value === null || value === ''
|
||||
|
||||
const toValueArray = (value: unknown) => {
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
}
|
||||
if (isEmptyValue(value)) {
|
||||
return []
|
||||
}
|
||||
return [value]
|
||||
}
|
||||
|
||||
const escapeHtml = (value: unknown) => {
|
||||
return String(value)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
const tryFormatDate = (value: unknown) => {
|
||||
if (isEmptyValue(value)) {
|
||||
return ''
|
||||
}
|
||||
const formatted = formatDate(value as Date | number | string)
|
||||
return formatted === 'Invalid Date' ? escapeHtml(value) : formatted
|
||||
}
|
||||
|
||||
const formatDateValue = (value: unknown) => {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => tryFormatDate(item)).join(' ~ ')
|
||||
}
|
||||
return tryFormatDate(value)
|
||||
}
|
||||
|
||||
const formatPrimitiveValue = (value: unknown): string => {
|
||||
if (isEmptyValue(value)) {
|
||||
return ''
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
.map((item) => formatPrimitiveValue(item))
|
||||
.filter((s) => isNotEmptyString(s))
|
||||
.join(', ')
|
||||
}
|
||||
if (typeof value === 'boolean') {
|
||||
return value ? '是' : '否'
|
||||
}
|
||||
if (isPrintableRecord(value)) {
|
||||
const displayValue =
|
||||
getRecordValue(value, 'label') ??
|
||||
getRecordValue(value, 'name') ??
|
||||
getRecordValue(value, 'url') ??
|
||||
getRecordValue(value, 'value') ??
|
||||
JSON.stringify(value)
|
||||
return escapeHtml(displayValue)
|
||||
}
|
||||
return escapeHtml(value)
|
||||
}
|
||||
|
||||
const createImageHtml = (url: string) => {
|
||||
const imgEl = document.createElement('img')
|
||||
imgEl.setAttribute('src', url)
|
||||
imgEl.setAttribute('style', 'max-width: 600px; max-height: 300px;')
|
||||
return imgEl.outerHTML
|
||||
}
|
||||
|
||||
const renderImageListHtml = (value: unknown) => {
|
||||
return toValueArray(value)
|
||||
.map((item) => {
|
||||
let url: string | undefined
|
||||
if (typeof item === 'string') {
|
||||
url = item
|
||||
} else if (isPrintableRecord(item)) {
|
||||
const recordUrl = getRecordValue(item, 'url')
|
||||
url = recordUrl ? String(recordUrl) : undefined
|
||||
}
|
||||
return url ? createImageHtml(url) : ''
|
||||
})
|
||||
.filter((s) => isNotEmptyString(s))
|
||||
.join('<br/>')
|
||||
}
|
||||
|
||||
const createFileLinkHtml = (file: unknown) => {
|
||||
const record = isPrintableRecord(file) ? file : undefined
|
||||
const recordUrl = record ? getRecordValue(record, 'url') : undefined
|
||||
const url = typeof file === 'string' ? file : String(recordUrl ?? '')
|
||||
if (!url) {
|
||||
return ''
|
||||
}
|
||||
const linkEl = document.createElement('a')
|
||||
linkEl.setAttribute('href', url)
|
||||
linkEl.setAttribute('target', '_blank')
|
||||
linkEl.setAttribute('rel', 'noopener noreferrer')
|
||||
const fallbackName = url.slice(Math.max(0, url.lastIndexOf('/') + 1)) || url
|
||||
const recordName = record ? getRecordValue(record, 'name') : undefined
|
||||
linkEl.textContent = recordName ? String(recordName) : fallbackName
|
||||
return linkEl.outerHTML
|
||||
}
|
||||
|
||||
const renderFileListHtml = (value: unknown) => {
|
||||
return toValueArray(value)
|
||||
.map((item) => createFileLinkHtml(item))
|
||||
.filter((s) => isNotEmptyString(s))
|
||||
.join('<br/>')
|
||||
}
|
||||
|
||||
const mapValuesWithOptions = (value: unknown, options: FormFieldOption[] = []) => {
|
||||
const values = toValueArray(value)
|
||||
const labels = values
|
||||
.map((item) => {
|
||||
const matched = options.find(
|
||||
(option) => option?.value === item || String(option?.value ?? '') === String(item)
|
||||
)
|
||||
return escapeHtml(matched?.label ?? String(item))
|
||||
})
|
||||
.filter((s) => isNotEmptyString(s))
|
||||
return labels.join(', ')
|
||||
}
|
||||
|
||||
const flattenAreaTree = (list: AreaNode[] = [], map: Map<string, string> = new Map()) => {
|
||||
list.forEach((item) => {
|
||||
if (item.id !== undefined) {
|
||||
map.set(String(item.id), item.name)
|
||||
}
|
||||
if (Array.isArray(item.children) && item.children.length > 0) {
|
||||
flattenAreaTree(item.children, map)
|
||||
}
|
||||
})
|
||||
return map
|
||||
}
|
||||
|
||||
const mapValueWithLabelMap = (
|
||||
value: unknown,
|
||||
labelMap: Map<string, string>,
|
||||
separator = ', '
|
||||
) => {
|
||||
const values = toValueArray(value)
|
||||
const labels = values
|
||||
.map((item) => escapeHtml(labelMap.get(String(item)) ?? String(item)))
|
||||
.filter((s) => isNotEmptyString(s))
|
||||
return labels.length > 0 ? labels.join(escapeHtml(separator)) : formatPrimitiveValue(values)
|
||||
}
|
||||
|
||||
const getTypedDictOptions = (dictType: string, valueType: string) => {
|
||||
switch (valueType) {
|
||||
case 'bool':
|
||||
case 'boolean':
|
||||
return getBoolDictOptions(dictType)
|
||||
case 'int':
|
||||
case 'number':
|
||||
return getIntDictOptions(dictType)
|
||||
case 'str':
|
||||
case 'string':
|
||||
return getStrDictOptions(dictType)
|
||||
default:
|
||||
return getDictOptions(dictType)
|
||||
}
|
||||
}
|
||||
|
||||
const loadPrintLookupMaps = async (formFieldsObj: FormFieldRule[]) => {
|
||||
const hasAreaSelect = formFieldsObj.some((item) => item.type === 'AreaSelect')
|
||||
const hasUserSelect = formFieldsObj.some((item) => item.type === 'UserSelect')
|
||||
const hasDeptSelect = formFieldsObj.some((item) => item.type === 'DeptSelect')
|
||||
|
||||
const [areaList, userList, deptList] = await Promise.all([
|
||||
hasAreaSelect ? AreaApi.getAreaTree() : Promise.resolve([]),
|
||||
hasUserSelect ? UserApi.getSimpleUserList() : Promise.resolve([]),
|
||||
hasDeptSelect ? DeptApi.getSimpleDeptList() : Promise.resolve([])
|
||||
])
|
||||
|
||||
return {
|
||||
areaMap: flattenAreaTree(areaList as AreaNode[]),
|
||||
deptMap: new Map((deptList ?? []).map((item) => [String(item.id), item.name] as const)),
|
||||
userMap: new Map(
|
||||
(userList ?? []).map((item) => [String(item.id), item.nickname ?? item.username] as const)
|
||||
)
|
||||
} satisfies PrintLookupMaps
|
||||
}
|
||||
|
||||
const formatPrintField = (
|
||||
rule: FormFieldRule,
|
||||
value: unknown,
|
||||
lookupMaps: PrintLookupMaps
|
||||
) => {
|
||||
const type = String(rule.type ?? '')
|
||||
|
||||
switch (type) {
|
||||
case 'AreaSelect': {
|
||||
const separator = String(getRuleProp(rule, 'separator') || '/')
|
||||
return mapValueWithLabelMap(value, lookupMaps.areaMap, separator)
|
||||
}
|
||||
case 'cascader':
|
||||
case 'checkbox':
|
||||
case 'radio':
|
||||
case 'select':
|
||||
case 'treeSelect': {
|
||||
const options = getRuleProp(rule, 'options')
|
||||
return Array.isArray(options) && options.length > 0
|
||||
? mapValuesWithOptions(value, options as FormFieldOption[])
|
||||
: formatPrimitiveValue(value)
|
||||
}
|
||||
case 'date':
|
||||
case 'DatePicker':
|
||||
case 'datePicker':
|
||||
case 'daterange':
|
||||
case 'datetime':
|
||||
case 'datetimerange':
|
||||
case 'month':
|
||||
case 'monthrange':
|
||||
case 'RangePicker':
|
||||
case 'rangePicker':
|
||||
case 'TimePicker':
|
||||
case 'timePicker':
|
||||
case 'TimeRangePicker':
|
||||
case 'timeRangePicker':
|
||||
return formatDateValue(value)
|
||||
case 'DeptSelect': {
|
||||
if (String(getRuleProp(rule, 'returnType')) === 'name') {
|
||||
return formatPrimitiveValue(value)
|
||||
}
|
||||
return mapValueWithLabelMap(value, lookupMaps.deptMap)
|
||||
}
|
||||
case 'DictSelect': {
|
||||
const dictType = getRuleProp(rule, 'dictType')
|
||||
if (typeof dictType !== 'string' || !dictType) {
|
||||
return formatPrimitiveValue(value)
|
||||
}
|
||||
const valueType = String(getRuleProp(rule, 'valueType') ?? '')
|
||||
return mapValuesWithOptions(value, getTypedDictOptions(dictType, valueType))
|
||||
}
|
||||
case 'FileUpload':
|
||||
case 'UploadFile':
|
||||
return renderFileListHtml(value)
|
||||
case 'IframeComponent': {
|
||||
const propsObj = rule.props
|
||||
const propsUrl = isPrintableRecord(propsObj) ? String(getRecordValue(propsObj, 'url') ?? '') : ''
|
||||
const iframeUrl = isEmptyValue(value) ? propsUrl : String(value ?? '')
|
||||
return iframeUrl ? createFileLinkHtml(iframeUrl) : ''
|
||||
}
|
||||
case 'ImagesUpload':
|
||||
case 'ImageUpload':
|
||||
case 'UploadImg':
|
||||
case 'UploadImgs':
|
||||
return renderImageListHtml(value)
|
||||
case 'switch': {
|
||||
if (isEmptyValue(value)) return '否'
|
||||
const checkedVal = getRuleProp(rule, 'checkedValue') ?? getRuleProp(rule, 'activeValue')
|
||||
const isChecked =
|
||||
checkedVal !== undefined && checkedVal !== null ? value === checkedVal : Boolean(value)
|
||||
return isChecked ? '是' : '否'
|
||||
}
|
||||
case 'Editor':
|
||||
case 'Tinymce':
|
||||
return isEmptyValue(value) ? '' : String(value)
|
||||
case 'UserSelect': {
|
||||
if (String(getRuleProp(rule, 'returnType')) === 'name') {
|
||||
return formatPrimitiveValue(value)
|
||||
}
|
||||
return mapValueWithLabelMap(value, lookupMaps.userMap)
|
||||
}
|
||||
default:
|
||||
return formatPrimitiveValue(value)
|
||||
}
|
||||
}
|
||||
|
||||
const initPrintDataMap = () => {
|
||||
printDataMap.value['startUser'] = printData.value.processInstance.startUser.nickname
|
||||
printDataMap.value['startUserDept'] = printData.value.processInstance.startUser.deptName
|
||||
if (!printData.value) return
|
||||
|
||||
printDataMap.value['startUser'] = printData.value.processInstance.startUser?.nickname || ''
|
||||
printDataMap.value['startUserDept'] = printData.value.processInstance.startUser?.deptName || ''
|
||||
printDataMap.value['processName'] = printData.value.processInstance.name
|
||||
printDataMap.value['processNum'] = printData.value.processInstance.id
|
||||
printDataMap.value['processNum'] = String(printData.value.processInstance.id ?? '')
|
||||
printDataMap.value['startTime'] = formatDate(printData.value.processInstance.startTime)
|
||||
printDataMap.value['endTime'] = formatDate(printData.value.processInstance.endTime)
|
||||
printDataMap.value['processStatus'] = getDictLabel(
|
||||
printDataMap.value['processStatus'] = String(getDictLabel(
|
||||
DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS,
|
||||
printData.value.processInstance.status
|
||||
)
|
||||
) ?? '')
|
||||
printDataMap.value['printUser'] = userName.value
|
||||
printDataMap.value['printTime'] = printTime.value
|
||||
}
|
||||
|
||||
const getPrintTemplateHTML = () => {
|
||||
if (!printData.value?.printTemplateHtml) return ''
|
||||
|
||||
const parser = new DOMParser()
|
||||
let doc = parser.parseFromString(printData.value.printTemplateHtml, 'text/html')
|
||||
const doc = parser.parseFromString(printData.value.printTemplateHtml, 'text/html')
|
||||
// table 添加border
|
||||
let tables = doc.querySelectorAll('table')
|
||||
const tables = doc.querySelectorAll('table')
|
||||
tables.forEach((item) => {
|
||||
item.setAttribute('border', '1')
|
||||
item.setAttribute('style', (item.getAttribute('style') || '') + 'border-collapse:collapse;')
|
||||
})
|
||||
// 替换 mentions
|
||||
let mentions = doc.querySelectorAll('[data-w-e-type="mention"]')
|
||||
const mentions = doc.querySelectorAll('[data-w-e-type="mention"]')
|
||||
mentions.forEach((item) => {
|
||||
const mentionId = JSON.parse(decodeURIComponent(item.getAttribute('data-info') ?? ''))['id']
|
||||
item.innerHTML = printDataMap.value[mentionId] ?? ''
|
||||
})
|
||||
// 替换流程记录
|
||||
let processRecords = doc.querySelectorAll('[data-w-e-type="process-record"]')
|
||||
let processRecordTable: Element = document.createElement('table')
|
||||
const processRecords = doc.querySelectorAll('[data-w-e-type="process-record"]')
|
||||
const processRecordTable: Element = document.createElement('table')
|
||||
if (processRecords.length > 0) {
|
||||
// 构建流程记录html
|
||||
processRecordTable.setAttribute('border', '1')
|
||||
|
|
@ -114,15 +423,15 @@ const getPrintTemplateHTML = () => {
|
|||
headTd.setAttribute('colspan', '2')
|
||||
headTd.setAttribute('width', 'auto')
|
||||
headTd.setAttribute('style', 'text-align: center;')
|
||||
headTd.innerHTML = '流程节点'
|
||||
headTd.textContent = '流程节点'
|
||||
headTr.appendChild(headTd)
|
||||
processRecordTable.appendChild(headTr)
|
||||
printData.value.tasks.forEach((item) => {
|
||||
const tr = document.createElement('tr')
|
||||
const td1 = document.createElement('td')
|
||||
td1.innerHTML = item.name
|
||||
td1.textContent = item.name
|
||||
const td2 = document.createElement('td')
|
||||
td2.innerHTML = item.description
|
||||
td2.textContent = item.description
|
||||
tr.appendChild(td1)
|
||||
tr.appendChild(td2)
|
||||
processRecordTable.appendChild(tr)
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
<el-tab-pane label="审批详情" name="form">
|
||||
<div class="form-scroll-area">
|
||||
<el-scrollbar>
|
||||
<el-row>
|
||||
<el-row :gutter="40">
|
||||
<el-col :span="17" class="!flex !flex-col formCol">
|
||||
<!-- 表单信息 -->
|
||||
<div
|
||||
|
|
@ -52,14 +52,14 @@
|
|||
class="form-box flex flex-col mb-30px flex-1"
|
||||
>
|
||||
<!-- 情况一:流程表单 -->
|
||||
<el-col v-if="processDefinition?.formType === BpmModelFormType.NORMAL">
|
||||
<div v-if="processDefinition?.formType === BpmModelFormType.NORMAL">
|
||||
<form-create
|
||||
v-model="detailForm.value"
|
||||
v-model:api="fApi"
|
||||
:option="detailForm.option"
|
||||
:rule="detailForm.rule"
|
||||
/>
|
||||
</el-col>
|
||||
</div>
|
||||
<!-- 情况二:业务表单 -->
|
||||
<div v-if="processDefinition?.formType === BpmModelFormType.CUSTOM">
|
||||
<BusinessFormComponent :id="processInstance.businessKey" />
|
||||
|
|
|
|||
|
|
@ -112,6 +112,7 @@ const list = ref([]) // 列表的数据
|
|||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
sceneType: 1,
|
||||
followUpStatus: false,
|
||||
transformStatus: false
|
||||
})
|
||||
|
|
|
|||
|
|
@ -11,12 +11,7 @@
|
|||
<el-input v-model="formData.name" placeholder="请输入配置名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="配置描述" prop="description">
|
||||
<el-input
|
||||
v-model="formData.description"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入配置描述"
|
||||
/>
|
||||
<el-input v-model="formData.description" placeholder="请输入配置描述" />
|
||||
</el-form-item>
|
||||
<el-form-item label="告警级别" prop="level">
|
||||
<el-select v-model="formData.level" placeholder="请选择告警级别">
|
||||
|
|
@ -83,6 +78,27 @@
|
|||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="formData.receiveTypes?.includes(IotAlertReceiveTypeEnum.SMS)"
|
||||
label="短信模板"
|
||||
prop="smsTemplateCode"
|
||||
>
|
||||
<SmsTemplateSelect v-model="formData.smsTemplateCode" />
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="formData.receiveTypes?.includes(IotAlertReceiveTypeEnum.MAIL)"
|
||||
label="邮件模板"
|
||||
prop="mailTemplateCode"
|
||||
>
|
||||
<MailTemplateSelect v-model="formData.mailTemplateCode" />
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="formData.receiveTypes?.includes(IotAlertReceiveTypeEnum.NOTIFY)"
|
||||
label="站内信模板"
|
||||
prop="notifyTemplateCode"
|
||||
>
|
||||
<NotifyTemplateSelect v-model="formData.notifyTemplateCode" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
|
||||
|
|
@ -95,7 +111,11 @@ import { AlertConfigApi, AlertConfig } from '@/api/iot/alert/config'
|
|||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||
import { CommonStatusEnum } from '@/utils/constants'
|
||||
import { RuleSceneApi } from '@/api/iot/rule/scene'
|
||||
import { IotAlertReceiveTypeEnum } from '@/views/iot/utils/constants'
|
||||
import * as UserApi from '@/api/system/user'
|
||||
import MailTemplateSelect from '@/views/system/mail/template/components/MailTemplateSelect.vue'
|
||||
import NotifyTemplateSelect from '@/views/system/notify/template/components/NotifyTemplateSelect.vue'
|
||||
import SmsTemplateSelect from '@/views/system/sms/template/components/SmsTemplateSelect.vue'
|
||||
|
||||
/** IoT 告警配置 表单 */
|
||||
defineOptions({ name: 'AlertConfigForm' })
|
||||
|
|
@ -107,7 +127,19 @@ const dialogVisible = ref(false) // 弹窗的是否展示
|
|||
const dialogTitle = ref('') // 弹窗的标题
|
||||
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||
const formType = ref('') // 表单的类型:create - 新增;update - 修改
|
||||
const formData = ref({
|
||||
const formData = ref<{
|
||||
id?: number
|
||||
name?: string
|
||||
description?: string
|
||||
level?: number
|
||||
status?: number
|
||||
sceneRuleIds: number[]
|
||||
receiveUserIds: number[]
|
||||
receiveTypes: number[]
|
||||
smsTemplateCode?: string
|
||||
mailTemplateCode?: string
|
||||
notifyTemplateCode?: string
|
||||
}>({
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
description: undefined,
|
||||
|
|
@ -115,9 +147,12 @@ const formData = ref({
|
|||
status: CommonStatusEnum.ENABLE,
|
||||
sceneRuleIds: [],
|
||||
receiveUserIds: [],
|
||||
receiveTypes: []
|
||||
receiveTypes: [],
|
||||
smsTemplateCode: undefined,
|
||||
mailTemplateCode: undefined,
|
||||
notifyTemplateCode: undefined
|
||||
})
|
||||
const formRules = reactive({
|
||||
const formRules = reactive<Record<string, any>>({
|
||||
name: [{ required: true, message: '配置名称不能为空', trigger: 'blur' }],
|
||||
level: [{ required: true, message: '告警级别不能为空', trigger: 'blur' }],
|
||||
status: [{ required: true, message: '配置状态不能为空', trigger: 'blur' }],
|
||||
|
|
@ -131,6 +166,47 @@ const formRef = ref() // 表单 Ref
|
|||
const sceneRuleOptions = ref<any[]>([])
|
||||
const userOptions = ref<UserApi.UserVO[]>([])
|
||||
|
||||
/** 按接收类型同步模板校验规则 */
|
||||
const syncTemplateFormRules = () => {
|
||||
const types = formData.value.receiveTypes || []
|
||||
if (types.includes(IotAlertReceiveTypeEnum.SMS)) {
|
||||
formRules.smsTemplateCode = [{ required: true, message: '短信模板不能为空', trigger: 'change' }]
|
||||
} else {
|
||||
delete formRules.smsTemplateCode
|
||||
}
|
||||
if (types.includes(IotAlertReceiveTypeEnum.MAIL)) {
|
||||
formRules.mailTemplateCode = [
|
||||
{ required: true, message: '邮件模板不能为空', trigger: 'change' }
|
||||
]
|
||||
} else {
|
||||
delete formRules.mailTemplateCode
|
||||
}
|
||||
if (types.includes(IotAlertReceiveTypeEnum.NOTIFY)) {
|
||||
formRules.notifyTemplateCode = [
|
||||
{ required: true, message: '站内信模板不能为空', trigger: 'change' }
|
||||
]
|
||||
} else {
|
||||
delete formRules.notifyTemplateCode
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => formData.value.receiveTypes,
|
||||
(types) => {
|
||||
if (!types?.includes(IotAlertReceiveTypeEnum.SMS)) {
|
||||
formData.value.smsTemplateCode = undefined
|
||||
}
|
||||
if (!types?.includes(IotAlertReceiveTypeEnum.MAIL)) {
|
||||
formData.value.mailTemplateCode = undefined
|
||||
}
|
||||
if (!types?.includes(IotAlertReceiveTypeEnum.NOTIFY)) {
|
||||
formData.value.notifyTemplateCode = undefined
|
||||
}
|
||||
syncTemplateFormRules()
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
/** 打开弹窗 */
|
||||
const open = async (type: string, id?: number) => {
|
||||
dialogVisible.value = true
|
||||
|
|
@ -143,6 +219,7 @@ const open = async (type: string, id?: number) => {
|
|||
formLoading.value = true
|
||||
try {
|
||||
formData.value = await AlertConfigApi.getAlertConfig(id)
|
||||
syncTemplateFormRules()
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
|
|
@ -156,10 +233,12 @@ defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
|||
/** 加载选项数据 */
|
||||
const loadOptions = async () => {
|
||||
try {
|
||||
// 加载场景联动规则选项
|
||||
sceneRuleOptions.value = await RuleSceneApi.getSimpleRuleSceneList()
|
||||
// 加载用户选项
|
||||
userOptions.value = await UserApi.getSimpleUserList()
|
||||
const [scenes, users] = await Promise.all([
|
||||
RuleSceneApi.getSimpleRuleSceneList(),
|
||||
UserApi.getSimpleUserList()
|
||||
])
|
||||
sceneRuleOptions.value = scenes
|
||||
userOptions.value = users
|
||||
} catch (error) {
|
||||
console.error('加载选项数据失败:', error)
|
||||
}
|
||||
|
|
@ -199,8 +278,12 @@ const resetForm = () => {
|
|||
status: CommonStatusEnum.ENABLE,
|
||||
sceneRuleIds: [],
|
||||
receiveUserIds: [],
|
||||
receiveTypes: []
|
||||
receiveTypes: [],
|
||||
smsTemplateCode: undefined,
|
||||
mailTemplateCode: undefined,
|
||||
notifyTemplateCode: undefined
|
||||
}
|
||||
syncTemplateFormRules()
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -308,9 +308,9 @@
|
|||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="设备状态" align="center" prop="status">
|
||||
<el-table-column label="设备状态" align="center" prop="state">
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.IOT_DEVICE_STATE" :value="scope.row.status" />
|
||||
<dict-tag :type="DICT_TYPE.IOT_DEVICE_STATE" :value="scope.row.state" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
|
|
|
|||
|
|
@ -56,21 +56,28 @@ watch([urlPrefix, urlPath], () => {
|
|||
config.value.url = fullUrl.value
|
||||
})
|
||||
|
||||
const syncUrlFields = (url?: string) => {
|
||||
if (url?.startsWith('https://')) {
|
||||
urlPrefix.value = 'https://'
|
||||
urlPath.value = url.substring(8)
|
||||
} else if (url?.startsWith('http://')) {
|
||||
urlPrefix.value = 'http://'
|
||||
urlPath.value = url.substring(7)
|
||||
} else {
|
||||
urlPath.value = url ?? ''
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => config.value?.url,
|
||||
(url) => syncUrlFields(url),
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
/** 组件初始化 */
|
||||
onMounted(() => {
|
||||
if (!isEmpty(config.value)) {
|
||||
// 初始化 URL
|
||||
if (config.value.url) {
|
||||
if (config.value.url.startsWith('https://')) {
|
||||
urlPrefix.value = 'https://'
|
||||
urlPath.value = config.value.url.substring(8)
|
||||
} else if (config.value.url.startsWith('http://')) {
|
||||
urlPrefix.value = 'http://'
|
||||
urlPath.value = config.value.url.substring(7)
|
||||
} else {
|
||||
urlPath.value = config.value.url
|
||||
}
|
||||
}
|
||||
syncUrlFields(config.value.url)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
:title="drawerTitle"
|
||||
size="80%"
|
||||
direction="rtl"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-click-modal="true"
|
||||
:close-on-press-escape="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
|
|
@ -12,9 +12,9 @@
|
|||
<!-- 基础信息配置 -->
|
||||
<BasicInfoSection v-model="formData" :rules="formRules" />
|
||||
<!-- 触发器配置 -->
|
||||
<TriggerSection v-model:triggers="formData.triggers" />
|
||||
<TriggerSection ref="triggerSectionRef" v-model:triggers="formData.triggers" />
|
||||
<!-- 执行器配置 -->
|
||||
<ActionSection v-model:actions="formData.actions" />
|
||||
<ActionSection ref="actionSectionRef" v-model:actions="formData.actions" />
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="drawer-footer">
|
||||
|
|
@ -38,11 +38,9 @@ import TriggerSection from './sections/TriggerSection.vue'
|
|||
import ActionSection from './sections/ActionSection.vue'
|
||||
import { IotSceneRule } from '@/api/iot/rule/scene'
|
||||
import { RuleSceneApi } from '@/api/iot/rule/scene'
|
||||
import {
|
||||
IotRuleSceneTriggerTypeEnum,
|
||||
IotRuleSceneActionTypeEnum,
|
||||
isDeviceTrigger
|
||||
} from '@/views/iot/utils/constants'
|
||||
import { IotRuleSceneTriggerTypeEnum } from '@/views/iot/utils/constants'
|
||||
import { validateActionItem, validateTriggerItem } from '@/views/iot/utils/sceneRule'
|
||||
import type { Trigger, Action } from '@/api/iot/rule/scene'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { CommonStatusEnum } from '@/utils/constants'
|
||||
|
||||
|
|
@ -91,66 +89,34 @@ const createDefaultFormData = (): IotSceneRule => {
|
|||
}
|
||||
|
||||
const formRef = ref() // 表单引用
|
||||
const triggerSectionRef = ref<{
|
||||
validateAllTriggers: () => Promise<boolean>
|
||||
clearAllTriggerValidate: () => void
|
||||
}>()
|
||||
const actionSectionRef = ref<{
|
||||
validateAllActions: () => Promise<boolean>
|
||||
clearAllActionValidate: () => void
|
||||
}>()
|
||||
const formData = ref<IotSceneRule>(createDefaultFormData()) // 表单数据
|
||||
|
||||
/**
|
||||
* 触发器校验器
|
||||
* 触发器校验器(兜底,与主条件 UI 规则一致)
|
||||
* @param _rule 校验规则(未使用)
|
||||
* @param value 校验值
|
||||
* @param callback 回调函数
|
||||
*/
|
||||
const validateTriggers = (_rule: any, value: any, callback: any) => {
|
||||
const validateTriggers = (_rule: any, value: Trigger[], callback: any) => {
|
||||
if (!value || !Array.isArray(value) || value.length === 0) {
|
||||
callback(new Error('至少需要一个触发器'))
|
||||
return
|
||||
}
|
||||
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
const trigger = value[i]
|
||||
|
||||
// 校验触发器类型
|
||||
if (!trigger.type) {
|
||||
callback(new Error(`触发器 ${i + 1}: 触发器类型不能为空`))
|
||||
const error = validateTriggerItem(value[i], i)
|
||||
if (error) {
|
||||
callback(new Error(error))
|
||||
return
|
||||
}
|
||||
|
||||
// 校验设备触发器
|
||||
if (isDeviceTrigger(trigger.type)) {
|
||||
if (!trigger.productId) {
|
||||
callback(new Error(`触发器 ${i + 1}: 产品不能为空`))
|
||||
return
|
||||
}
|
||||
if (!trigger.deviceId) {
|
||||
callback(new Error(`触发器 ${i + 1}: 设备不能为空`))
|
||||
return
|
||||
}
|
||||
if (!trigger.identifier) {
|
||||
callback(new Error(`触发器 ${i + 1}: 物模型标识符不能为空`))
|
||||
return
|
||||
}
|
||||
// 事件上报 / 服务调用:operator 由前端自动设为 '=',参数值留空表示"事件 / 调用发生即匹配"
|
||||
const isEventOrService =
|
||||
trigger.type === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST ||
|
||||
trigger.type === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
|
||||
if (!isEventOrService) {
|
||||
if (!trigger.operator) {
|
||||
callback(new Error(`触发器 ${i + 1}: 操作符不能为空`))
|
||||
return
|
||||
}
|
||||
if (trigger.value === undefined || trigger.value === null || trigger.value === '') {
|
||||
callback(new Error(`触发器 ${i + 1}: 参数值不能为空`))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 校验定时触发器
|
||||
if (trigger.type === IotRuleSceneTriggerTypeEnum.TIMER) {
|
||||
if (!trigger.cronExpression) {
|
||||
callback(new Error(`触发器 ${i + 1}: CRON表达式不能为空`))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
callback()
|
||||
|
|
@ -162,59 +128,18 @@ const validateTriggers = (_rule: any, value: any, callback: any) => {
|
|||
* @param value 校验值
|
||||
* @param callback 回调函数
|
||||
*/
|
||||
const validateActions = (_rule: any, value: any, callback: any) => {
|
||||
const validateActions = (_rule: any, value: Action[], callback: any) => {
|
||||
if (!value || !Array.isArray(value) || value.length === 0) {
|
||||
callback(new Error('至少需要一个执行器'))
|
||||
return
|
||||
}
|
||||
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
const action = value[i]
|
||||
|
||||
// 校验执行器类型
|
||||
if (!action.type) {
|
||||
callback(new Error(`执行器 ${i + 1}: 执行器类型不能为空`))
|
||||
const error = validateActionItem(value[i], i)
|
||||
if (error) {
|
||||
callback(new Error(error))
|
||||
return
|
||||
}
|
||||
|
||||
// 校验设备控制执行器
|
||||
if (
|
||||
action.type === IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET ||
|
||||
action.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE
|
||||
) {
|
||||
if (!action.productId) {
|
||||
callback(new Error(`执行器 ${i + 1}: 产品不能为空`))
|
||||
return
|
||||
}
|
||||
if (!action.deviceId) {
|
||||
callback(new Error(`执行器 ${i + 1}: 设备不能为空`))
|
||||
return
|
||||
}
|
||||
|
||||
// 服务调用需要验证服务标识符
|
||||
if (action.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE) {
|
||||
if (!action.identifier) {
|
||||
callback(new Error(`执行器 ${i + 1}: 服务不能为空`))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (!action.params || Object.keys(action.params).length === 0) {
|
||||
callback(new Error(`执行器 ${i + 1}: 参数配置不能为空`))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 校验告警执行器
|
||||
if (
|
||||
action.type === IotRuleSceneActionTypeEnum.ALERT_TRIGGER ||
|
||||
action.type === IotRuleSceneActionTypeEnum.ALERT_RECOVER
|
||||
) {
|
||||
if (!action.alertConfigId) {
|
||||
callback(new Error(`执行器 ${i + 1}: 告警配置不能为空`))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
callback()
|
||||
|
|
@ -247,10 +172,22 @@ const drawerTitle = computed(() => (isEdit.value ? '编辑场景联动规则' :
|
|||
|
||||
/** 提交表单 */
|
||||
const handleSubmit = async () => {
|
||||
// 校验表单
|
||||
if (!formRef.value) return
|
||||
const valid = await formRef.value.validate()
|
||||
if (!valid) return
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
const mainConditionValid = await triggerSectionRef.value?.validateAllTriggers?.()
|
||||
if (mainConditionValid === false) {
|
||||
return
|
||||
}
|
||||
|
||||
const actionValid = await actionSectionRef.value?.validateAllActions?.()
|
||||
if (actionValid === false) {
|
||||
return
|
||||
}
|
||||
|
||||
// 提交请求
|
||||
submitLoading.value = true
|
||||
|
|
@ -320,6 +257,8 @@ watch(drawerVisible, async (visible) => {
|
|||
// 重置表单验证状态
|
||||
await nextTick()
|
||||
formRef.value?.clearValidate()
|
||||
triggerSectionRef.value?.clearAllTriggerValidate?.()
|
||||
actionSectionRef.value?.clearAllActionValidate?.()
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
<!-- 告警配置组件 -->
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<el-form-item label="告警配置" required>
|
||||
<el-form
|
||||
ref="innerFormRef"
|
||||
:model="formModel"
|
||||
:rules="formRules"
|
||||
label-width="110px"
|
||||
class="w-full"
|
||||
>
|
||||
<el-form-item label="告警配置" prop="alertConfigId" required>
|
||||
<el-select
|
||||
v-model="localValue"
|
||||
placeholder="请选择告警配置"
|
||||
|
|
@ -16,15 +22,24 @@
|
|||
:key="config.id"
|
||||
:label="config.name"
|
||||
:value="config.id"
|
||||
/>
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>{{ config.name }}</span>
|
||||
<el-tag :type="config.enabled ? 'success' : 'danger'" size="small">
|
||||
{{ config.enabled ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { useVModel } from '@vueuse/core'
|
||||
import { AlertConfigApi } from '@/api/iot/alert/config'
|
||||
import { buildAlertConfigRules } from '@/views/iot/utils/sceneRule'
|
||||
|
||||
/** 告警配置组件 */
|
||||
defineOptions({ name: 'AlertConfig' })
|
||||
|
|
@ -38,9 +53,15 @@ const emit = defineEmits<{
|
|||
}>()
|
||||
|
||||
const localValue = useVModel(props, 'modelValue', emit)
|
||||
const innerFormRef = ref<FormInstance>()
|
||||
const formRules = buildAlertConfigRules()
|
||||
|
||||
const loading = ref(false) // 加载状态
|
||||
const alertConfigs = ref<any[]>([]) // 告警配置列表
|
||||
const formModel = computed(() => ({
|
||||
alertConfigId: localValue.value
|
||||
}))
|
||||
|
||||
const loading = ref(false)
|
||||
const alertConfigs = ref<any[]>([])
|
||||
|
||||
/**
|
||||
* 处理选择变化事件
|
||||
|
|
@ -48,19 +69,44 @@ const alertConfigs = ref<any[]>([]) // 告警配置列表
|
|||
*/
|
||||
const handleChange = (value?: number) => {
|
||||
emit('update:modelValue', value)
|
||||
nextTick(() => {
|
||||
innerFormRef.value?.validateField('alertConfigId').catch(() => {})
|
||||
})
|
||||
}
|
||||
|
||||
/** 加载告警配置列表 */
|
||||
const loadAlertConfigs = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
alertConfigs.value = (await AlertConfigApi.getSimpleAlertConfigList()) || []
|
||||
const data = await AlertConfigApi.getAlertConfigPage({
|
||||
pageNo: 1,
|
||||
pageSize: 100,
|
||||
enabled: true
|
||||
})
|
||||
alertConfigs.value = data.list || []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时加载数据
|
||||
const validate = async (): Promise<boolean> => {
|
||||
if (!innerFormRef.value) {
|
||||
return true
|
||||
}
|
||||
try {
|
||||
await innerFormRef.value.validate()
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const clearValidate = () => {
|
||||
innerFormRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
defineExpose({ validate, clearValidate })
|
||||
|
||||
onMounted(() => {
|
||||
loadAlertConfigs()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
<!-- 单个条件配置组件 -->
|
||||
<template>
|
||||
<div class="flex flex-col gap-16px">
|
||||
<el-form
|
||||
ref="innerFormRef"
|
||||
:model="condition"
|
||||
:rules="conditionRules"
|
||||
label-width="110px"
|
||||
class="flex flex-col gap-16px"
|
||||
>
|
||||
<!-- 条件类型选择 -->
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="条件类型" required>
|
||||
<el-form-item label="条件类型" prop="type" required>
|
||||
<el-select
|
||||
:model-value="condition.type"
|
||||
@update:model-value="(value) => updateConditionField('type', value)"
|
||||
|
|
@ -26,7 +32,7 @@
|
|||
<!-- 产品设备选择 - 设备相关条件的公共部分 -->
|
||||
<el-row v-if="isDeviceCondition" :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="产品" required>
|
||||
<el-form-item label="产品" prop="productId" required>
|
||||
<ProductSelector
|
||||
:model-value="condition.productId"
|
||||
@update:model-value="(value) => updateConditionField('productId', value)"
|
||||
|
|
@ -35,7 +41,7 @@
|
|||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="设备" required>
|
||||
<el-form-item label="设备" prop="deviceId" required>
|
||||
<DeviceSelector
|
||||
:model-value="condition.deviceId"
|
||||
@update:model-value="(value) => updateConditionField('deviceId', value)"
|
||||
|
|
@ -51,11 +57,9 @@
|
|||
v-if="condition.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS"
|
||||
class="flex flex-col gap-16px"
|
||||
>
|
||||
<!-- 状态和操作符选择 -->
|
||||
<el-row :gutter="16">
|
||||
<!-- 操作符选择 -->
|
||||
<el-col :span="12">
|
||||
<el-form-item label="操作符" required>
|
||||
<el-form-item label="操作符" prop="operator" required>
|
||||
<el-select
|
||||
:model-value="condition.operator"
|
||||
@update:model-value="(value) => updateConditionField('operator', value)"
|
||||
|
|
@ -72,9 +76,8 @@
|
|||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<!-- 状态选择 -->
|
||||
<el-col :span="12">
|
||||
<el-form-item label="设备状态" required>
|
||||
<el-form-item label="设备状态" prop="param" required>
|
||||
<el-select
|
||||
:model-value="condition.param"
|
||||
@update:model-value="(value) => updateConditionField('param', value)"
|
||||
|
|
@ -98,11 +101,9 @@
|
|||
v-else-if="condition.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY"
|
||||
class="space-y-16px"
|
||||
>
|
||||
<!-- 属性配置 -->
|
||||
<el-row :gutter="16">
|
||||
<!-- 属性/事件/服务选择 -->
|
||||
<el-col :span="6">
|
||||
<el-form-item label="监控项" required>
|
||||
<el-form-item label="监控项" prop="identifier" required>
|
||||
<PropertySelector
|
||||
:model-value="condition.identifier"
|
||||
@update:model-value="(value) => updateConditionField('identifier', value)"
|
||||
|
|
@ -114,9 +115,8 @@
|
|||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<!-- 操作符选择 -->
|
||||
<el-col :span="6">
|
||||
<el-form-item label="操作符" required>
|
||||
<el-form-item label="操作符" prop="operator" required>
|
||||
<OperatorSelector
|
||||
:model-value="condition.operator"
|
||||
@update:model-value="(value) => updateConditionField('operator', value)"
|
||||
|
|
@ -126,9 +126,8 @@
|
|||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<!-- 值输入 -->
|
||||
<el-col :span="12">
|
||||
<el-form-item label="比较值" required>
|
||||
<el-form-item label="比较值" prop="param" required>
|
||||
<ValueInput
|
||||
:model-value="condition.param"
|
||||
@update:model-value="(value) => updateConditionField('param', value)"
|
||||
|
|
@ -146,11 +145,13 @@
|
|||
v-else-if="condition.type === IotRuleSceneTriggerConditionTypeEnum.CURRENT_TIME"
|
||||
:model-value="condition"
|
||||
@update:model-value="updateCondition"
|
||||
@field-change="handleCurrentTimeFieldChange"
|
||||
/>
|
||||
</div>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { useVModel } from '@vueuse/core'
|
||||
import CurrentTimeConditionConfig from './CurrentTimeConditionConfig.vue'
|
||||
import ProductSelector from '../selectors/ProductSelector.vue'
|
||||
|
|
@ -165,6 +166,7 @@ import {
|
|||
getConditionTypeOptions,
|
||||
IoTDeviceStatusEnum
|
||||
} from '@/views/iot/utils/constants'
|
||||
import { buildSubConditionRules } from '@/views/iot/utils/sceneRule'
|
||||
|
||||
/** 单个条件配置组件 */
|
||||
defineOptions({ name: 'ConditionConfig' })
|
||||
|
|
@ -203,24 +205,32 @@ const statusOperatorOptions = [
|
|||
]
|
||||
|
||||
const condition = useVModel(props, 'modelValue', emit)
|
||||
const innerFormRef = ref<FormInstance>()
|
||||
const propertyType = ref<string>('string')
|
||||
const propertyConfig = ref<any>(null)
|
||||
|
||||
const propertyType = ref<string>('string') // 属性类型
|
||||
const propertyConfig = ref<any>(null) // 属性配置
|
||||
const isDeviceCondition = computed(() => {
|
||||
return (
|
||||
condition.value.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS ||
|
||||
condition.value.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY
|
||||
)
|
||||
}) // 计算属性:判断是否为设备相关条件
|
||||
})
|
||||
|
||||
const conditionRules = computed(() =>
|
||||
buildSubConditionRules(condition.value.type, () => condition.value.operator)
|
||||
)
|
||||
|
||||
/**
|
||||
* 更新条件字段
|
||||
* @param field 字段名
|
||||
* @param value 字段值
|
||||
*/
|
||||
const updateConditionField = (field: any, value: any) => {
|
||||
const updateConditionField = (field: keyof TriggerCondition, value: any) => {
|
||||
;(condition.value as any)[field] = value
|
||||
emit('update:modelValue', condition.value)
|
||||
nextTick(() => {
|
||||
innerFormRef.value?.validateField(field as string).catch(() => {})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -232,46 +242,56 @@ const updateCondition = (newCondition: TriggerCondition) => {
|
|||
emit('update:modelValue', condition.value)
|
||||
}
|
||||
|
||||
/** 当前时间子组件字段变更后触发校验 */
|
||||
const handleCurrentTimeFieldChange = (field: string) => {
|
||||
nextTick(() => {
|
||||
innerFormRef.value?.validateField(field).catch(() => {})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理条件类型变化事件
|
||||
* @param type 条件类型
|
||||
*/
|
||||
const handleConditionTypeChange = (type: number) => {
|
||||
// 根据条件类型清理字段
|
||||
const isCurrentTime = type === IotRuleSceneTriggerConditionTypeEnum.CURRENT_TIME
|
||||
const isDeviceStatus = type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS
|
||||
|
||||
// 清理标识符字段(时间条件和设备状态条件都不需要)
|
||||
if (isCurrentTime || isDeviceStatus) {
|
||||
condition.value.identifier = undefined
|
||||
}
|
||||
|
||||
// 清理设备相关字段(仅时间条件需要)
|
||||
if (isCurrentTime) {
|
||||
condition.value.productId = undefined
|
||||
condition.value.deviceId = undefined
|
||||
}
|
||||
|
||||
// 设置默认操作符
|
||||
condition.value.operator = isCurrentTime
|
||||
? 'at_time'
|
||||
: IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value
|
||||
|
||||
// 清空参数值
|
||||
condition.value.param = ''
|
||||
emit('update:modelValue', condition.value)
|
||||
nextTick(() => clearValidate())
|
||||
}
|
||||
|
||||
/** 处理产品变化事件 */
|
||||
const handleProductChange = (_: number) => {
|
||||
// 产品变化时清空设备和属性
|
||||
const handleProductChange = () => {
|
||||
condition.value.deviceId = undefined
|
||||
condition.value.identifier = ''
|
||||
emit('update:modelValue', condition.value)
|
||||
nextTick(() => {
|
||||
innerFormRef.value?.clearValidate(['deviceId', 'identifier'])
|
||||
})
|
||||
}
|
||||
|
||||
/** 处理设备变化事件 */
|
||||
const handleDeviceChange = (_: number) => {
|
||||
// 设备变化时清空属性
|
||||
const handleDeviceChange = () => {
|
||||
condition.value.identifier = ''
|
||||
emit('update:modelValue', condition.value)
|
||||
nextTick(() => {
|
||||
innerFormRef.value?.clearValidate('identifier')
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -281,17 +301,37 @@ const handleDeviceChange = (_: number) => {
|
|||
const handlePropertyChange = (propertyInfo: { type: string; config: any }) => {
|
||||
propertyType.value = propertyInfo.type
|
||||
propertyConfig.value = propertyInfo.config
|
||||
|
||||
// 重置操作符和值
|
||||
condition.value.operator = IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value
|
||||
condition.value.param = ''
|
||||
emit('update:modelValue', condition.value)
|
||||
}
|
||||
|
||||
/** 处理操作符变化事件 */
|
||||
const handleOperatorChange = () => {
|
||||
// 重置值
|
||||
condition.value.param = ''
|
||||
emit('update:modelValue', condition.value)
|
||||
nextTick(() => {
|
||||
innerFormRef.value?.validateField('param').catch(() => {})
|
||||
})
|
||||
}
|
||||
|
||||
const validate = async (): Promise<boolean> => {
|
||||
if (!innerFormRef.value) {
|
||||
return true
|
||||
}
|
||||
try {
|
||||
await innerFormRef.value.validate()
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const clearValidate = () => {
|
||||
innerFormRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
defineExpose({ validate, clearValidate })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<el-row :gutter="16">
|
||||
<!-- 时间操作符选择 -->
|
||||
<el-col :span="8">
|
||||
<el-form-item label="时间条件" required>
|
||||
<el-form-item label="时间条件" prop="operator" required>
|
||||
<el-select
|
||||
:model-value="condition.operator"
|
||||
@update:model-value="(value) => updateConditionField('operator', value)"
|
||||
|
|
@ -31,7 +31,7 @@
|
|||
|
||||
<!-- 时间值输入 -->
|
||||
<el-col :span="8">
|
||||
<el-form-item label="时间值" required>
|
||||
<el-form-item label="时间值" prop="param" required>
|
||||
<el-time-picker
|
||||
v-if="needsTimeInput"
|
||||
:model-value="timeValue"
|
||||
|
|
@ -59,7 +59,7 @@
|
|||
|
||||
<!-- 第二个时间值(范围条件) -->
|
||||
<el-col :span="8" v-if="needsSecondTimeInput">
|
||||
<el-form-item label="结束时间" required>
|
||||
<el-form-item label="结束时间" prop="param" required>
|
||||
<el-time-picker
|
||||
v-if="needsTimeInput"
|
||||
:model-value="timeValue2"
|
||||
|
|
@ -99,6 +99,7 @@ const props = defineProps<{
|
|||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: TriggerCondition): void
|
||||
(e: 'field-change', field: string): void
|
||||
}>()
|
||||
|
||||
const condition = useVModel(props, 'modelValue', emit)
|
||||
|
|
@ -187,8 +188,9 @@ const timeValue2 = computed(() => {
|
|||
* @param field 字段名
|
||||
* @param value 字段值
|
||||
*/
|
||||
const updateConditionField = (field: any, value: any) => {
|
||||
const updateConditionField = (field: keyof TriggerCondition, value: any) => {
|
||||
condition.value[field] = value
|
||||
emit('field-change', field)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -199,12 +201,12 @@ const handleTimeValueChange = (value: string) => {
|
|||
const currentParams = condition.value.param ? condition.value.param.split(',') : []
|
||||
currentParams[0] = value || ''
|
||||
|
||||
// 如果是范围条件,保留第二个值;否则只保留第一个值
|
||||
if (needsSecondTimeInput.value) {
|
||||
condition.value.param = currentParams.slice(0, 2).join(',')
|
||||
} else {
|
||||
condition.value.param = currentParams[0]
|
||||
}
|
||||
emit('field-change', 'param')
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -215,6 +217,7 @@ const handleTimeValue2Change = (value: string) => {
|
|||
const currentParams = condition.value.param ? condition.value.param.split(',') : ['']
|
||||
currentParams[1] = value || ''
|
||||
condition.value.param = currentParams.slice(0, 2).join(',')
|
||||
emit('field-change', 'param')
|
||||
}
|
||||
|
||||
/** 监听操作符变化,清理不相关的时间值 */
|
||||
|
|
@ -222,13 +225,12 @@ watch(
|
|||
() => condition.value.operator,
|
||||
(newOperator) => {
|
||||
if (newOperator === IotRuleSceneTriggerTimeOperatorEnum.TODAY.value) {
|
||||
// 今日条件不需要时间参数
|
||||
condition.value.param = ''
|
||||
} else if (!needsSecondTimeInput.value) {
|
||||
// 非范围条件只保留第一个时间值
|
||||
const currentParams = condition.value.param ? condition.value.param.split(',') : []
|
||||
condition.value.param = currentParams[0] || ''
|
||||
}
|
||||
emit('field-change', 'param')
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,20 @@
|
|||
<!-- 设备控制配置组件 -->
|
||||
<template>
|
||||
<div class="flex flex-col gap-16px">
|
||||
<!-- 产品和设备选择 - 与触发器保持一致的分离式选择器 -->
|
||||
<el-form
|
||||
ref="innerFormRef"
|
||||
:model="action"
|
||||
:rules="formRules"
|
||||
label-width="110px"
|
||||
class="flex flex-col gap-16px"
|
||||
>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="产品" required>
|
||||
<el-form-item label="产品" prop="productId" required>
|
||||
<ProductSelector v-model="action.productId" @change="handleProductChange" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="设备" required>
|
||||
<el-form-item label="设备" prop="deviceId" required>
|
||||
<DeviceSelector
|
||||
v-model="action.deviceId"
|
||||
:product-id="action.productId"
|
||||
|
|
@ -21,7 +26,7 @@
|
|||
|
||||
<!-- 服务选择 - 服务调用类型时显示 -->
|
||||
<div v-if="action.productId && isServiceInvokeAction" class="space-y-16px">
|
||||
<el-form-item label="服务" required>
|
||||
<el-form-item label="服务" prop="identifier" required>
|
||||
<el-select
|
||||
v-model="action.identifier"
|
||||
placeholder="请选择服务"
|
||||
|
|
@ -47,9 +52,8 @@
|
|||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 服务参数配置 -->
|
||||
<div v-if="action.identifier" class="space-y-16px">
|
||||
<el-form-item label="服务参数" required>
|
||||
<el-form-item label="服务参数" prop="params" required>
|
||||
<JsonParamsInput
|
||||
v-model="paramsValue"
|
||||
type="service"
|
||||
|
|
@ -62,8 +66,7 @@
|
|||
|
||||
<!-- 控制参数配置 - 属性设置类型时显示 -->
|
||||
<div v-if="action.productId && isPropertySetAction" class="space-y-16px">
|
||||
<!-- 参数配置 -->
|
||||
<el-form-item label="参数" required>
|
||||
<el-form-item label="参数" prop="params" required>
|
||||
<JsonParamsInput
|
||||
v-model="paramsValue"
|
||||
type="property"
|
||||
|
|
@ -72,10 +75,11 @@
|
|||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { useVModel } from '@vueuse/core'
|
||||
import ProductSelector from '../selectors/ProductSelector.vue'
|
||||
import DeviceSelector from '../selectors/DeviceSelector.vue'
|
||||
|
|
@ -88,6 +92,7 @@ import {
|
|||
IoTDataSpecsDataTypeEnum
|
||||
} from '@/views/iot/utils/constants'
|
||||
import { ThingModelApi } from '@/api/iot/thingmodel'
|
||||
import { buildDeviceControlRules } from '@/views/iot/utils/sceneRule'
|
||||
|
||||
/** 设备控制配置组件 */
|
||||
defineOptions({ name: 'DeviceControlConfig' })
|
||||
|
|
@ -101,54 +106,71 @@ const emit = defineEmits<{
|
|||
}>()
|
||||
|
||||
const action = useVModel(props, 'modelValue', emit)
|
||||
const innerFormRef = ref<FormInstance>()
|
||||
|
||||
const thingModelProperties = ref<ThingModelProperty[]>([]) // 物模型属性列表
|
||||
const loadingThingModel = ref(false) // 物模型加载状态
|
||||
const selectedService = ref<ThingModelService | null>(null) // 选中的服务对象
|
||||
const serviceList = ref<ThingModelService[]>([]) // 服务列表
|
||||
const loadingServices = ref(false) // 服务加载状态
|
||||
const formRules = computed(() => {
|
||||
const rules = buildDeviceControlRules(action.value.type)
|
||||
|
||||
if (isServiceInvokeAction.value) {
|
||||
if (!action.value.productId) {
|
||||
delete rules.identifier
|
||||
delete rules.params
|
||||
} else if (!action.value.identifier) {
|
||||
delete rules.params
|
||||
}
|
||||
}
|
||||
|
||||
if (isPropertySetAction.value && !action.value.productId) {
|
||||
delete rules.params
|
||||
}
|
||||
|
||||
return rules
|
||||
})
|
||||
|
||||
const thingModelProperties = ref<ThingModelProperty[]>([])
|
||||
const loadingThingModel = ref(false)
|
||||
const selectedService = ref<ThingModelService | null>(null)
|
||||
const serviceList = ref<ThingModelService[]>([])
|
||||
const loadingServices = ref(false)
|
||||
|
||||
// 参数值的计算属性,用于双向绑定
|
||||
const paramsValue = computed({
|
||||
get: () => {
|
||||
// 如果 params 是对象,转换为 JSON 字符串(兼容旧数据)
|
||||
if (action.value.params && typeof action.value.params === 'object') {
|
||||
return JSON.stringify(action.value.params, null, 2)
|
||||
}
|
||||
// 如果 params 已经是字符串,直接返回
|
||||
return action.value.params || ''
|
||||
},
|
||||
set: (value: string) => {
|
||||
// 直接保存为 JSON 字符串,不进行解析转换
|
||||
action.value.params = value.trim() || ''
|
||||
nextTick(() => {
|
||||
innerFormRef.value?.validateField('params').catch(() => {})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 计算属性:是否为属性设置类型
|
||||
const isPropertySetAction = computed(() => {
|
||||
return action.value.type === IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET
|
||||
})
|
||||
|
||||
// 计算属性:是否为服务调用类型
|
||||
const isServiceInvokeAction = computed(() => {
|
||||
return action.value.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE
|
||||
})
|
||||
|
||||
/**
|
||||
* 处理产品变化事件
|
||||
* @param productId 产品 ID
|
||||
*/
|
||||
const validateField = (field: string) => {
|
||||
nextTick(() => {
|
||||
innerFormRef.value?.validateField(field).catch(() => {})
|
||||
})
|
||||
}
|
||||
|
||||
const handleProductChange = (productId?: number) => {
|
||||
// 当产品变化时,清空设备选择和参数配置
|
||||
if (action.value.productId !== productId) {
|
||||
action.value.deviceId = undefined
|
||||
action.value.identifier = undefined // 清空服务标识符
|
||||
action.value.params = '' // 清空参数,保存为空字符串
|
||||
selectedService.value = null // 清空选中的服务
|
||||
serviceList.value = [] // 清空服务列表
|
||||
action.value.identifier = undefined
|
||||
action.value.params = ''
|
||||
selectedService.value = null
|
||||
serviceList.value = []
|
||||
}
|
||||
|
||||
// 加载新产品的物模型属性或服务列表
|
||||
if (productId) {
|
||||
if (isPropertySetAction.value) {
|
||||
loadThingModelProperties(productId)
|
||||
|
|
@ -156,47 +178,37 @@ const handleProductChange = (productId?: number) => {
|
|||
loadServiceList(productId)
|
||||
}
|
||||
}
|
||||
|
||||
validateField('productId')
|
||||
nextTick(() => {
|
||||
innerFormRef.value?.clearValidate(['deviceId', 'identifier', 'params'])
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理设备变化事件
|
||||
* @param deviceId 设备 ID
|
||||
*/
|
||||
const handleDeviceChange = (deviceId?: number) => {
|
||||
// 当设备变化时,清空参数配置
|
||||
if (action.value.deviceId !== deviceId) {
|
||||
action.value.params = '' // 清空参数,保存为空字符串
|
||||
action.value.params = ''
|
||||
}
|
||||
validateField('deviceId')
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理服务变化事件
|
||||
* @param serviceIdentifier 服务标识符
|
||||
*/
|
||||
const handleServiceChange = (serviceIdentifier?: string) => {
|
||||
// 根据服务标识符找到对应的服务对象
|
||||
const service = serviceList.value.find((s) => s.identifier === serviceIdentifier) || null
|
||||
selectedService.value = service
|
||||
|
||||
// 当服务变化时,清空参数配置
|
||||
action.value.params = ''
|
||||
|
||||
// 如果选择了服务且有输入参数,生成默认参数结构
|
||||
if (service && service.inputParams && service.inputParams.length > 0) {
|
||||
const defaultParams = {}
|
||||
const defaultParams: Record<string, unknown> = {}
|
||||
service.inputParams.forEach((param) => {
|
||||
defaultParams[param.identifier] = getDefaultValueForParam(param)
|
||||
})
|
||||
// 将默认参数转换为 JSON 字符串保存
|
||||
action.value.params = JSON.stringify(defaultParams, null, 2)
|
||||
}
|
||||
|
||||
validateField('identifier')
|
||||
validateField('params')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取物模型TSL数据
|
||||
* @param productId 产品ID
|
||||
* @returns 物模型TSL数据
|
||||
*/
|
||||
const getThingModelTSL = async (productId: number) => {
|
||||
if (!productId) return null
|
||||
|
||||
|
|
@ -208,10 +220,6 @@ const getThingModelTSL = async (productId: number) => {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载物模型属性(可写属性)
|
||||
* @param productId 产品ID
|
||||
*/
|
||||
const loadThingModelProperties = async (productId: number) => {
|
||||
if (!productId) {
|
||||
thingModelProperties.value = []
|
||||
|
|
@ -227,7 +235,6 @@ const loadThingModelProperties = async (productId: number) => {
|
|||
return
|
||||
}
|
||||
|
||||
// 过滤出可写的属性(accessMode 包含 'w')
|
||||
thingModelProperties.value = tslData.properties.filter(
|
||||
(property: ThingModelProperty) =>
|
||||
property.accessMode &&
|
||||
|
|
@ -242,10 +249,6 @@ const loadThingModelProperties = async (productId: number) => {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载服务列表
|
||||
* @param productId 产品ID
|
||||
*/
|
||||
const loadServiceList = async (productId: number) => {
|
||||
if (!productId) {
|
||||
serviceList.value = []
|
||||
|
|
@ -270,27 +273,14 @@ const loadServiceList = async (productId: number) => {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从TSL加载服务信息(用于编辑模式回显)
|
||||
* @param productId 产品ID
|
||||
* @param serviceIdentifier 服务标识符
|
||||
*/
|
||||
const loadServiceFromTSL = async (productId: number, serviceIdentifier: string) => {
|
||||
// 先加载服务列表
|
||||
await loadServiceList(productId)
|
||||
|
||||
// 然后设置选中的服务
|
||||
const service = serviceList.value.find((s: any) => s.identifier === serviceIdentifier)
|
||||
const service = serviceList.value.find((s) => s.identifier === serviceIdentifier)
|
||||
if (service) {
|
||||
selectedService.value = service
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据参数类型获取默认值
|
||||
* @param param 参数对象
|
||||
* @returns 默认值
|
||||
*/
|
||||
const getDefaultValueForParam = (param: any) => {
|
||||
switch (param.dataType) {
|
||||
case IoTDataSpecsDataTypeEnum.INT:
|
||||
|
|
@ -303,7 +293,6 @@ const getDefaultValueForParam = (param: any) => {
|
|||
case IoTDataSpecsDataTypeEnum.TEXT:
|
||||
return ''
|
||||
case IoTDataSpecsDataTypeEnum.ENUM:
|
||||
// 如果有枚举值,使用第一个
|
||||
if (param.dataSpecs?.dataSpecsList && param.dataSpecs.dataSpecsList.length > 0) {
|
||||
return param.dataSpecs.dataSpecsList[0].value
|
||||
}
|
||||
|
|
@ -313,44 +302,52 @@ const getDefaultValueForParam = (param: any) => {
|
|||
}
|
||||
}
|
||||
|
||||
const isInitialized = ref(false) // 防止重复初始化的标志
|
||||
const isInitialized = ref(false)
|
||||
|
||||
/**
|
||||
* 初始化组件数据
|
||||
*/
|
||||
const initializeComponent = async () => {
|
||||
if (isInitialized.value) return
|
||||
|
||||
const currentAction = action.value
|
||||
if (!currentAction) return
|
||||
|
||||
// 如果已经选择了产品且是属性设置类型,加载物模型
|
||||
if (currentAction.productId && isPropertySetAction.value) {
|
||||
await loadThingModelProperties(currentAction.productId)
|
||||
}
|
||||
|
||||
// 如果是服务调用类型且已有标识符,初始化服务选择
|
||||
if (currentAction.productId && isServiceInvokeAction.value && currentAction.identifier) {
|
||||
// 加载物模型TSL以获取服务信息
|
||||
await loadServiceFromTSL(currentAction.productId, currentAction.identifier)
|
||||
}
|
||||
|
||||
isInitialized.value = true
|
||||
}
|
||||
|
||||
/** 组件初始化 */
|
||||
const validate = async (): Promise<boolean> => {
|
||||
if (!innerFormRef.value) {
|
||||
return true
|
||||
}
|
||||
try {
|
||||
await innerFormRef.value.validate()
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const clearValidate = () => {
|
||||
innerFormRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
defineExpose({ validate, clearValidate })
|
||||
|
||||
onMounted(() => {
|
||||
initializeComponent()
|
||||
})
|
||||
|
||||
/** 监听关键字段的变化,避免深度监听导致的性能问题 */
|
||||
watch(
|
||||
() => [action.value.productId, action.value.type, action.value.identifier],
|
||||
async ([newProductId, , newIdentifier], [oldProductId, , oldIdentifier]) => {
|
||||
// 避免初始化时的重复调用
|
||||
if (!isInitialized.value) return
|
||||
|
||||
// 产品变化时重新加载数据
|
||||
if (newProductId !== oldProductId) {
|
||||
if (newProductId && isPropertySetAction.value) {
|
||||
await loadThingModelProperties(newProductId as number)
|
||||
|
|
@ -359,18 +356,22 @@ watch(
|
|||
}
|
||||
}
|
||||
|
||||
// 服务标识符变化时更新选中的服务
|
||||
if (
|
||||
newIdentifier !== oldIdentifier &&
|
||||
newProductId &&
|
||||
isServiceInvokeAction.value &&
|
||||
newIdentifier
|
||||
) {
|
||||
const service = serviceList.value.find((s: any) => s.identifier === newIdentifier)
|
||||
const service = serviceList.value.find((s) => s.identifier === newIdentifier)
|
||||
if (service) {
|
||||
selectedService.value = service
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => action.value.type,
|
||||
() => nextTick(() => clearValidate())
|
||||
)
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@
|
|||
|
||||
<!-- 主条件内容配置 -->
|
||||
<MainConditionInnerConfig
|
||||
ref="mainConditionRef"
|
||||
:model-value="trigger"
|
||||
@update:model-value="updateCondition"
|
||||
:trigger-type="trigger.type"
|
||||
|
|
@ -118,6 +119,7 @@
|
|||
</div>
|
||||
|
||||
<SubConditionGroupConfig
|
||||
:ref="(el) => setSubGroupRef(el, subGroupIndex)"
|
||||
:model-value="subGroup"
|
||||
@update:model-value="(value) => updateSubGroup(subGroupIndex, value)"
|
||||
:trigger-type="trigger.type"
|
||||
|
|
@ -184,6 +186,22 @@ const emit = defineEmits<{
|
|||
}>()
|
||||
|
||||
const trigger = useVModel(props, 'modelValue', emit)
|
||||
const mainConditionRef = ref<InstanceType<typeof MainConditionInnerConfig>>()
|
||||
|
||||
type SubConditionGroupExpose = {
|
||||
validate: () => Promise<boolean>
|
||||
clearValidate: () => void
|
||||
}
|
||||
|
||||
const subGroupRefs = ref<Record<number, SubConditionGroupExpose>>({})
|
||||
|
||||
const setSubGroupRef = (el: unknown, index: number) => {
|
||||
if (el) {
|
||||
subGroupRefs.value[index] = el as SubConditionGroupExpose
|
||||
} else {
|
||||
delete subGroupRefs.value[index]
|
||||
}
|
||||
}
|
||||
|
||||
const maxSubGroups = 3 // 最多 3 个子条件组
|
||||
const maxConditionsPerGroup = 3 // 每组最多 3 个条件
|
||||
|
|
@ -248,4 +266,35 @@ const updateSubGroup = (index: number, subGroup: any) => {
|
|||
const removeConditionGroup = () => {
|
||||
trigger.value.conditionGroups = undefined
|
||||
}
|
||||
|
||||
/** 校验主条件及附加子条件组 */
|
||||
const validate = async (): Promise<boolean> => {
|
||||
const mainValid = (await mainConditionRef.value?.validate()) ?? true
|
||||
if (!mainValid) {
|
||||
return false
|
||||
}
|
||||
|
||||
const groups = trigger.value.conditionGroups
|
||||
if (!groups?.length) {
|
||||
return true
|
||||
}
|
||||
|
||||
for (let i = 0; i < groups.length; i++) {
|
||||
const subGroupRef = subGroupRefs.value[i]
|
||||
if (subGroupRef?.validate) {
|
||||
const valid = await subGroupRef.validate()
|
||||
if (!valid) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const clearValidate = () => {
|
||||
mainConditionRef.value?.clearValidate()
|
||||
Object.values(subGroupRefs.value).forEach((ref) => ref.clearValidate?.())
|
||||
}
|
||||
|
||||
defineExpose({ validate, clearValidate })
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
<template>
|
||||
<div class="space-y-16px">
|
||||
<el-form
|
||||
ref="innerFormRef"
|
||||
:model="condition"
|
||||
:rules="conditionRules"
|
||||
label-width="110px"
|
||||
class="space-y-16px"
|
||||
>
|
||||
<!-- 触发事件类型选择 -->
|
||||
<el-form-item label="触发事件类型" required>
|
||||
<el-select
|
||||
|
|
@ -22,7 +28,7 @@
|
|||
<!-- 产品设备选择 -->
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="产品" required>
|
||||
<el-form-item label="产品" prop="productId" required>
|
||||
<ProductSelector
|
||||
:model-value="condition.productId"
|
||||
@update:model-value="(value) => updateConditionField('productId', value)"
|
||||
|
|
@ -31,7 +37,7 @@
|
|||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="设备" required>
|
||||
<el-form-item label="设备" prop="deviceId" required>
|
||||
<DeviceSelector
|
||||
:model-value="condition.deviceId"
|
||||
@update:model-value="(value) => updateConditionField('deviceId', value)"
|
||||
|
|
@ -46,7 +52,7 @@
|
|||
<el-row :gutter="16">
|
||||
<!-- 属性/事件/服务选择 -->
|
||||
<el-col :span="6">
|
||||
<el-form-item label="监控项" required>
|
||||
<el-form-item label="监控项" prop="identifier" required>
|
||||
<PropertySelector
|
||||
:model-value="condition.identifier"
|
||||
@update:model-value="(value) => updateConditionField('identifier', value)"
|
||||
|
|
@ -60,7 +66,7 @@
|
|||
|
||||
<!-- 操作符选择 - 服务调用和事件上报不需要操作符 -->
|
||||
<el-col v-if="needsOperatorSelector" :span="6">
|
||||
<el-form-item label="操作符" required>
|
||||
<el-form-item label="操作符" prop="operator" required>
|
||||
<OperatorSelector
|
||||
:model-value="condition.operator"
|
||||
@update:model-value="(value) => updateConditionField('operator', value)"
|
||||
|
|
@ -71,7 +77,7 @@
|
|||
|
||||
<!-- 值输入 -->
|
||||
<el-col :span="isWideValueColumn ? 18 : 12">
|
||||
<el-form-item :label="valueInputLabel" required>
|
||||
<el-form-item :label="valueInputLabel" prop="value" :required="needsValueRequired">
|
||||
<!-- 服务调用参数配置 -->
|
||||
<JsonParamsInput
|
||||
v-if="triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE"
|
||||
|
|
@ -113,7 +119,7 @@
|
|||
<!-- 设备状态触发器使用简化的配置 -->
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="产品" required>
|
||||
<el-form-item label="产品" prop="productId" required>
|
||||
<ProductSelector
|
||||
:model-value="condition.productId"
|
||||
@update:model-value="(value) => updateConditionField('productId', value)"
|
||||
|
|
@ -122,7 +128,7 @@
|
|||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="设备" required>
|
||||
<el-form-item label="设备" prop="deviceId" required>
|
||||
<DeviceSelector
|
||||
:model-value="condition.deviceId"
|
||||
@update:model-value="(value) => updateConditionField('deviceId', value)"
|
||||
|
|
@ -134,7 +140,7 @@
|
|||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="6">
|
||||
<el-form-item label="操作符" required>
|
||||
<el-form-item label="操作符" prop="operator" required>
|
||||
<el-select
|
||||
:model-value="condition.operator"
|
||||
@update:model-value="(value) => updateConditionField('operator', value)"
|
||||
|
|
@ -149,11 +155,11 @@
|
|||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="参数" required>
|
||||
<el-form-item label="参数" prop="value" required>
|
||||
<el-select
|
||||
:model-value="condition.value"
|
||||
@update:model-value="(value) => updateConditionField('value', value)"
|
||||
placeholder="请选择操作符"
|
||||
placeholder="请选择设备状态"
|
||||
class="w-full"
|
||||
>
|
||||
<el-option
|
||||
|
|
@ -177,10 +183,11 @@
|
|||
此触发类型暂不需要配置额外条件
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import ProductSelector from '../selectors/ProductSelector.vue'
|
||||
import DeviceSelector from '../selectors/DeviceSelector.vue'
|
||||
import PropertySelector from '../selectors/PropertySelector.vue'
|
||||
|
|
@ -196,6 +203,7 @@ import {
|
|||
IotRuleSceneTriggerConditionParameterOperatorEnum,
|
||||
IoTDeviceStatusEnum
|
||||
} from '@/views/iot/utils/constants'
|
||||
import { buildMainConditionRules } from '@/views/iot/utils/sceneRule'
|
||||
import { useVModel } from '@vueuse/core'
|
||||
|
||||
/** 主条件内部配置组件 */
|
||||
|
|
@ -224,9 +232,12 @@ const deviceStatusChangeOptions = [
|
|||
]
|
||||
|
||||
const condition = useVModel(props, 'modelValue', emit)
|
||||
const innerFormRef = ref<FormInstance>()
|
||||
const propertyType = ref('') // 属性类型
|
||||
const propertyConfig = ref<any>(null) // 属性配置
|
||||
|
||||
const conditionRules = computed(() => buildMainConditionRules(props.triggerType))
|
||||
|
||||
// 计算属性:是否为设备属性触发器
|
||||
const isDevicePropertyTrigger = computed(() => {
|
||||
return (
|
||||
|
|
@ -250,6 +261,11 @@ const needsOperatorSelector = computed(() => {
|
|||
return !noOperatorTriggerTypes.includes(props.triggerType)
|
||||
})
|
||||
|
||||
// 比较值是否必填(属性上报必填,事件/服务可选)
|
||||
const needsValueRequired = computed(() => {
|
||||
return props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST
|
||||
})
|
||||
|
||||
// 计算属性:是否需要宽列布局(服务调用和事件上报不需要操作符列,所以值输入列更宽)
|
||||
const isWideValueColumn = computed(() => {
|
||||
const wideColumnTriggerTypes = [
|
||||
|
|
@ -282,13 +298,26 @@ const serviceConfig = computed(() => {
|
|||
return undefined
|
||||
})
|
||||
|
||||
/** 设备状态触发器默认操作符为「等于」 */
|
||||
const ensureDeviceStatusDefaults = () => {
|
||||
if (props.triggerType !== IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE) {
|
||||
return
|
||||
}
|
||||
if (!condition.value.operator) {
|
||||
condition.value.operator = IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新条件字段
|
||||
* @param field 字段名
|
||||
* @param value 字段值
|
||||
*/
|
||||
const updateConditionField = (field: any, value: any) => {
|
||||
const updateConditionField = (field: keyof Trigger, value: any) => {
|
||||
condition.value[field] = value
|
||||
nextTick(() => {
|
||||
innerFormRef.value?.validateField(field as string).catch(() => {})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -301,15 +330,19 @@ const handleTriggerTypeChange = (type: number) => {
|
|||
|
||||
/** 处理产品变化事件 */
|
||||
const handleProductChange = () => {
|
||||
// 产品变化时清空设备和属性
|
||||
condition.value.deviceId = undefined
|
||||
condition.value.identifier = ''
|
||||
nextTick(() => {
|
||||
innerFormRef.value?.clearValidate(['deviceId', 'identifier'])
|
||||
})
|
||||
}
|
||||
|
||||
/** 处理设备变化事件 */
|
||||
const handleDeviceChange = () => {
|
||||
// 设备变化时清空属性
|
||||
condition.value.identifier = ''
|
||||
nextTick(() => {
|
||||
innerFormRef.value?.clearValidate('identifier')
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -321,7 +354,6 @@ const handlePropertyChange = (propertyInfo: any) => {
|
|||
propertyType.value = propertyInfo.type
|
||||
propertyConfig.value = propertyInfo.config
|
||||
|
||||
// 对于事件上报和服务调用,自动设置操作符为 '='
|
||||
if (
|
||||
props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST ||
|
||||
props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
|
||||
|
|
@ -330,4 +362,36 @@ const handlePropertyChange = (propertyInfo: any) => {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 校验主条件表单 */
|
||||
const validate = async (): Promise<boolean> => {
|
||||
if (!innerFormRef.value || Object.keys(conditionRules.value).length === 0) {
|
||||
return true
|
||||
}
|
||||
try {
|
||||
await innerFormRef.value.validate()
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const clearValidate = () => {
|
||||
innerFormRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
defineExpose({ validate, clearValidate })
|
||||
|
||||
watch(
|
||||
() => props.triggerType,
|
||||
() => {
|
||||
ensureDeviceStatusDefaults()
|
||||
nextTick(() => clearValidate())
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
ensureDeviceStatusDefaults()
|
||||
})
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@
|
|||
|
||||
<div class="p-12px">
|
||||
<ConditionConfig
|
||||
:ref="(el) => setConditionRef(el, conditionIndex)"
|
||||
:model-value="condition"
|
||||
@update:model-value="(value) => updateCondition(conditionIndex, value)"
|
||||
:trigger-type="triggerType"
|
||||
|
|
@ -105,6 +106,44 @@ const subGroup = useVModel(props, 'modelValue', emit)
|
|||
|
||||
const maxConditions = computed(() => props.maxConditions || 3) // 最大条件数量
|
||||
|
||||
type ConditionConfigExpose = {
|
||||
validate: () => Promise<boolean>
|
||||
clearValidate: () => void
|
||||
}
|
||||
|
||||
const conditionRefs = ref<Record<number, ConditionConfigExpose>>({})
|
||||
|
||||
const setConditionRef = (el: unknown, index: number) => {
|
||||
if (el) {
|
||||
conditionRefs.value[index] = el as ConditionConfigExpose
|
||||
} else {
|
||||
delete conditionRefs.value[index]
|
||||
}
|
||||
}
|
||||
|
||||
/** 校验组内所有子条件 */
|
||||
const validate = async (): Promise<boolean> => {
|
||||
if (!subGroup.value?.length) {
|
||||
return false
|
||||
}
|
||||
for (let i = 0; i < subGroup.value.length; i++) {
|
||||
const conditionRef = conditionRefs.value[i]
|
||||
if (conditionRef?.validate) {
|
||||
const valid = await conditionRef.validate()
|
||||
if (!valid) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const clearValidate = () => {
|
||||
Object.values(conditionRefs.value).forEach((ref) => ref.clearValidate?.())
|
||||
}
|
||||
|
||||
defineExpose({ validate, clearValidate })
|
||||
|
||||
/** 添加条件 */
|
||||
const addCondition = async () => {
|
||||
// 确保 subGroup.value 是一个数组
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@
|
|||
</div>
|
||||
|
||||
<SubConditionGroupConfig
|
||||
:ref="(el) => setSubGroupRef(el, groupIndex)"
|
||||
:model-value="group"
|
||||
@update:model-value="(value) => updateConditionGroup(groupIndex, value)"
|
||||
:trigger-type="IotRuleSceneTriggerTypeEnum.TIMER"
|
||||
|
|
@ -131,6 +132,44 @@ const conditionGroups = useVModel(props, 'modelValue', emit)
|
|||
const maxGroups = 3 // 最多 3 个条件组
|
||||
const maxConditionsPerGroup = 3 // 每组最多 3 个条件
|
||||
|
||||
type SubConditionGroupExpose = {
|
||||
validate: () => Promise<boolean>
|
||||
clearValidate: () => void
|
||||
}
|
||||
|
||||
const subGroupRefs = ref<Record<number, SubConditionGroupExpose>>({})
|
||||
|
||||
const setSubGroupRef = (el: unknown, index: number) => {
|
||||
if (el) {
|
||||
subGroupRefs.value[index] = el as SubConditionGroupExpose
|
||||
} else {
|
||||
delete subGroupRefs.value[index]
|
||||
}
|
||||
}
|
||||
|
||||
/** 校验所有附加条件组 */
|
||||
const validate = async (): Promise<boolean> => {
|
||||
if (!conditionGroups.value?.length) {
|
||||
return true
|
||||
}
|
||||
for (let i = 0; i < conditionGroups.value.length; i++) {
|
||||
const subGroupRef = subGroupRefs.value[i]
|
||||
if (subGroupRef?.validate) {
|
||||
const valid = await subGroupRef.validate()
|
||||
if (!valid) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const clearValidate = () => {
|
||||
Object.values(subGroupRefs.value).forEach((ref) => ref.clearValidate?.())
|
||||
}
|
||||
|
||||
defineExpose({ validate, clearValidate })
|
||||
|
||||
/** 添加条件组 */
|
||||
const addConditionGroup = async () => {
|
||||
if (!conditionGroups.value) {
|
||||
|
|
|
|||
|
|
@ -141,6 +141,7 @@
|
|||
<script setup lang="ts">
|
||||
import { useVModel } from '@vueuse/core'
|
||||
import { InfoFilled } from '@element-plus/icons-vue'
|
||||
import { isEmptyVal } from '@/utils/is'
|
||||
import {
|
||||
IoTDataSpecsDataTypeEnum,
|
||||
JSON_PARAMS_INPUT_CONSTANTS,
|
||||
|
|
@ -329,7 +330,8 @@ const handleParamsChange = () => {
|
|||
|
||||
// 验证必填参数
|
||||
for (const param of paramsList.value) {
|
||||
if (param.required && (!parsed[param.identifier] || parsed[param.identifier] === '')) {
|
||||
const value = parsed[param.identifier]
|
||||
if (param.required && isEmptyVal(value)) {
|
||||
jsonError.value = JSON_PARAMS_INPUT_CONSTANTS.PARAM_REQUIRED_ERROR(param.name)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@
|
|||
<el-select
|
||||
:model-value="action.type"
|
||||
@update:model-value="(value) => updateActionType(index, value)"
|
||||
@change="(value) => onActionTypeChange(action, value)"
|
||||
@change="(value) => onActionTypeChange(action, value, index)"
|
||||
placeholder="请选择执行类型"
|
||||
class="w-full"
|
||||
>
|
||||
|
|
@ -92,6 +92,7 @@
|
|||
<!-- 设备控制配置 -->
|
||||
<DeviceControlConfig
|
||||
v-if="isDeviceAction(action.type)"
|
||||
:ref="(el) => setDeviceControlRef(el, index)"
|
||||
:model-value="action"
|
||||
@update:model-value="(value) => updateAction(index, value)"
|
||||
/>
|
||||
|
|
@ -99,6 +100,7 @@
|
|||
<!-- 告警配置 - 只有恢复告警时才显示 -->
|
||||
<AlertConfig
|
||||
v-if="action.type === IotRuleSceneActionTypeEnum.ALERT_RECOVER"
|
||||
:ref="(el) => setAlertConfigRef(el, index)"
|
||||
:model-value="action.alertConfigId"
|
||||
@update:model-value="(value) => updateActionAlertConfig(index, value)"
|
||||
/>
|
||||
|
|
@ -156,6 +158,66 @@ const emit = defineEmits<{
|
|||
|
||||
const actions = useVModel(props, 'actions', emit)
|
||||
|
||||
type ConfigExpose = {
|
||||
validate: () => Promise<boolean>
|
||||
clearValidate: () => void
|
||||
}
|
||||
|
||||
const deviceControlRefs = ref<Record<number, ConfigExpose>>({})
|
||||
const alertConfigRefs = ref<Record<number, ConfigExpose>>({})
|
||||
|
||||
const setDeviceControlRef = (el: unknown, index: number) => {
|
||||
if (el) {
|
||||
deviceControlRefs.value[index] = el as ConfigExpose
|
||||
} else {
|
||||
delete deviceControlRefs.value[index]
|
||||
}
|
||||
}
|
||||
|
||||
const setAlertConfigRef = (el: unknown, index: number) => {
|
||||
if (el) {
|
||||
alertConfigRefs.value[index] = el as ConfigExpose
|
||||
} else {
|
||||
delete alertConfigRefs.value[index]
|
||||
}
|
||||
}
|
||||
|
||||
/** 校验所有执行器配置 */
|
||||
const validateAllActions = async (): Promise<boolean> => {
|
||||
for (let i = 0; i < actions.value.length; i++) {
|
||||
const action = actions.value[i]
|
||||
|
||||
if (isDeviceAction(action.type)) {
|
||||
const deviceRef = deviceControlRefs.value[i]
|
||||
if (deviceRef?.validate) {
|
||||
const valid = await deviceRef.validate()
|
||||
if (!valid) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (action.type === IotRuleSceneActionTypeEnum.ALERT_RECOVER) {
|
||||
const alertRef = alertConfigRefs.value[i]
|
||||
if (alertRef?.validate) {
|
||||
const valid = await alertRef.validate()
|
||||
if (!valid) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const clearAllActionValidate = () => {
|
||||
Object.values(deviceControlRefs.value).forEach((ref) => ref.clearValidate?.())
|
||||
Object.values(alertConfigRefs.value).forEach((ref) => ref.clearValidate?.())
|
||||
}
|
||||
|
||||
defineExpose({ validateAllActions, clearAllActionValidate })
|
||||
|
||||
/** 获取执行器标签类型(用于 el-tag 的 type 属性) */
|
||||
const getActionTypeTag = (type: number): 'primary' | 'success' | 'info' | 'warning' | 'danger' => {
|
||||
const actionTypeTags = {
|
||||
|
|
@ -222,9 +284,8 @@ const removeAction = (index: number) => {
|
|||
* @param type 执行器类型
|
||||
*/
|
||||
const updateActionType = (index: number, type: number) => {
|
||||
const action = actions.value[index]
|
||||
onActionTypeChange(action, type) // 须在赋新值前调用 ,内部依赖 action.type 旧值
|
||||
action.type = type
|
||||
actions.value[index].type = type
|
||||
onActionTypeChange(actions.value[index], type, index)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -250,7 +311,7 @@ const updateActionAlertConfig = (index: number, alertConfigId?: number) => {
|
|||
* @param action 执行器对象
|
||||
* @param type 执行器类型
|
||||
*/
|
||||
const onActionTypeChange = (action: Action, type: number) => {
|
||||
const onActionTypeChange = (action: Action, type: number, index: number) => {
|
||||
// 清理不相关的配置,确保数据结构干净
|
||||
if (isDeviceAction(type)) {
|
||||
// 设备控制类型:清理告警配置,确保设备参数存在
|
||||
|
|
@ -269,5 +330,10 @@ const onActionTypeChange = (action: Action, type: number) => {
|
|||
action.params = undefined
|
||||
action.alertConfigId = undefined
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
deviceControlRefs.value[index]?.clearValidate?.()
|
||||
alertConfigRefs.value[index]?.clearValidate?.()
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@
|
|||
<!-- 设备触发配置 -->
|
||||
<DeviceTriggerConfig
|
||||
v-if="isDeviceTrigger(triggerItem.type)"
|
||||
:ref="(el) => setDeviceTriggerRef(el, index)"
|
||||
:model-value="triggerItem"
|
||||
:index="index"
|
||||
@update:model-value="(value) => updateTriggerDeviceConfig(index, value)"
|
||||
|
|
@ -93,6 +94,7 @@
|
|||
|
||||
<!-- 附加条件组配置 -->
|
||||
<TimerConditionGroupConfig
|
||||
:ref="(el) => setTimerConditionRef(el, index)"
|
||||
:model-value="triggerItem.conditionGroups"
|
||||
@update:model-value="(value) => updateTriggerConditionGroups(index, value)"
|
||||
/>
|
||||
|
|
@ -127,6 +129,7 @@ import type { Trigger, TriggerCondition } from '@/api/iot/rule/scene'
|
|||
import {
|
||||
getTriggerTypeLabel,
|
||||
IotRuleSceneTriggerTypeEnum,
|
||||
IotRuleSceneTriggerConditionParameterOperatorEnum,
|
||||
isDeviceTrigger
|
||||
} from '@/views/iot/utils/constants'
|
||||
|
||||
|
|
@ -143,6 +146,72 @@ const emit = defineEmits<{
|
|||
|
||||
const triggers = useVModel(props, 'triggers', emit)
|
||||
|
||||
type DeviceTriggerConfigExpose = {
|
||||
validate: () => Promise<boolean>
|
||||
clearValidate: () => void
|
||||
}
|
||||
|
||||
const deviceTriggerRefs = ref<Record<number, DeviceTriggerConfigExpose>>({})
|
||||
|
||||
type TimerConditionGroupExpose = {
|
||||
validate: () => Promise<boolean>
|
||||
clearValidate: () => void
|
||||
}
|
||||
|
||||
const timerConditionRefs = ref<Record<number, TimerConditionGroupExpose>>({})
|
||||
|
||||
const setDeviceTriggerRef = (el: unknown, index: number) => {
|
||||
if (el) {
|
||||
deviceTriggerRefs.value[index] = el as DeviceTriggerConfigExpose
|
||||
} else {
|
||||
delete deviceTriggerRefs.value[index]
|
||||
}
|
||||
}
|
||||
|
||||
const setTimerConditionRef = (el: unknown, index: number) => {
|
||||
if (el) {
|
||||
timerConditionRefs.value[index] = el as TimerConditionGroupExpose
|
||||
} else {
|
||||
delete timerConditionRefs.value[index]
|
||||
}
|
||||
}
|
||||
|
||||
/** 校验所有触发器(主条件 + 附加子条件组) */
|
||||
const validateAllTriggers = async (): Promise<boolean> => {
|
||||
for (let i = 0; i < triggers.value.length; i++) {
|
||||
const triggerItem = triggers.value[i]
|
||||
|
||||
if (isDeviceTrigger(triggerItem.type)) {
|
||||
const deviceConfig = deviceTriggerRefs.value[i]
|
||||
if (deviceConfig?.validate) {
|
||||
const valid = await deviceConfig.validate()
|
||||
if (!valid) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (triggerItem.type === IotRuleSceneTriggerTypeEnum.TIMER) {
|
||||
const timerConfig = timerConditionRefs.value[i]
|
||||
if (timerConfig?.validate) {
|
||||
const valid = await timerConfig.validate()
|
||||
if (!valid) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const clearAllTriggerValidate = () => {
|
||||
Object.values(deviceTriggerRefs.value).forEach((ref) => ref.clearValidate?.())
|
||||
Object.values(timerConditionRefs.value).forEach((ref) => ref.clearValidate?.())
|
||||
}
|
||||
|
||||
defineExpose({ validateAllTriggers, clearAllTriggerValidate })
|
||||
|
||||
/** 获取触发器标签类型(用于 el-tag 的 type 属性) */
|
||||
const getTriggerTagType = (type: number): 'primary' | 'success' | 'info' | 'warning' | 'danger' => {
|
||||
if (type === IotRuleSceneTriggerTypeEnum.TIMER) {
|
||||
|
|
@ -158,7 +227,7 @@ const addTrigger = () => {
|
|||
productId: undefined,
|
||||
deviceId: undefined,
|
||||
identifier: undefined,
|
||||
operator: undefined,
|
||||
operator: IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value,
|
||||
value: undefined,
|
||||
cronExpression: undefined,
|
||||
conditionGroups: [] // 空的条件组数组
|
||||
|
|
@ -218,7 +287,7 @@ const updateTriggerConditionGroups = (index: number, conditionGroups: TriggerCon
|
|||
* @param index 触发器索引
|
||||
* @param _ 触发器类型(未使用)
|
||||
*/
|
||||
const onTriggerTypeChange = (index: number, _: number) => {
|
||||
const onTriggerTypeChange = (index: number, type: number) => {
|
||||
const triggerItem = triggers.value[index]
|
||||
triggerItem.productId = undefined
|
||||
triggerItem.deviceId = undefined
|
||||
|
|
@ -227,6 +296,10 @@ const onTriggerTypeChange = (index: number, _: number) => {
|
|||
triggerItem.value = undefined
|
||||
triggerItem.cronExpression = undefined
|
||||
triggerItem.conditionGroups = []
|
||||
if (type === IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE) {
|
||||
triggerItem.operator = IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value
|
||||
}
|
||||
nextTick(() => deviceTriggerRefs.value[index]?.clearValidate?.())
|
||||
}
|
||||
|
||||
/** 初始化:确保至少有一个触发器 */
|
||||
|
|
|
|||
|
|
@ -25,6 +25,13 @@ export const IoTThingModelTypeEnum = {
|
|||
EVENT: 3 // 事件
|
||||
} as const
|
||||
|
||||
/** IoT 告警接收方式枚举,与后端 IotAlertReceiveTypeEnum 保持一致 */
|
||||
export const IotAlertReceiveTypeEnum = {
|
||||
SMS: 1, // 短信
|
||||
MAIL: 2, // 邮箱
|
||||
NOTIFY: 3 // 站内信
|
||||
} as const
|
||||
|
||||
/** IoT 设备消息的方法枚举 */
|
||||
export const IotDeviceMessageMethodEnum = {
|
||||
// ========== 设备状态 ==========
|
||||
|
|
|
|||
|
|
@ -0,0 +1,503 @@
|
|||
import type { FormItemRule } from 'element-plus'
|
||||
import type { Action, Trigger, TriggerCondition } from '@/api/iot/rule/scene'
|
||||
import { isEmptyVal } from '@/utils/is'
|
||||
import {
|
||||
IotRuleSceneActionTypeEnum,
|
||||
IotRuleSceneTriggerConditionTypeEnum,
|
||||
IotRuleSceneTriggerTimeOperatorEnum,
|
||||
IotRuleSceneTriggerTypeEnum,
|
||||
isDeviceTrigger
|
||||
} from './constants'
|
||||
|
||||
/** 创建 Element Plus 表单必填规则,统一触发方式和错误提示。 */
|
||||
const requiredRule = (message: string): FormItemRule => ({
|
||||
required: true,
|
||||
message,
|
||||
trigger: ['change', 'blur']
|
||||
})
|
||||
|
||||
/**
|
||||
* 构建触发器主条件的表单校验规则。
|
||||
*
|
||||
* 该规则与 MainConditionInnerConfig 中实际展示的字段保持一致:
|
||||
* - 设备状态变化:需要产品、设备、操作符、设备状态;
|
||||
* - 属性上报:需要产品、设备、监控项、操作符、比较值;
|
||||
* - 事件上报、服务调用:只需要产品、设备、监控项。
|
||||
*
|
||||
* @param triggerType 触发器类型
|
||||
* @returns Element Plus 表单 rules
|
||||
*/
|
||||
export function buildMainConditionRules(triggerType: number): Record<string, FormItemRule[]> {
|
||||
const base: Record<string, FormItemRule[]> = {
|
||||
productId: [requiredRule('请选择产品')],
|
||||
deviceId: [requiredRule('请选择设备')]
|
||||
}
|
||||
|
||||
// 设备状态变化使用固定状态枚举作为比较值。
|
||||
if (triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE) {
|
||||
return {
|
||||
...base,
|
||||
operator: [requiredRule('请选择操作符')],
|
||||
value: [requiredRule('请选择设备状态')]
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST ||
|
||||
triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST ||
|
||||
triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
|
||||
) {
|
||||
const rules: Record<string, FormItemRule[]> = {
|
||||
...base,
|
||||
identifier: [requiredRule('请选择监控项')]
|
||||
}
|
||||
|
||||
// 事件上报和服务调用只监听是否发生,不需要额外的操作符和比较值。
|
||||
const isEventOrService =
|
||||
triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST ||
|
||||
triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
|
||||
|
||||
if (!isEventOrService) {
|
||||
rules.operator = [requiredRule('请选择操作符')]
|
||||
rules.value = [requiredRule('请填写比较值')]
|
||||
}
|
||||
return rules
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验单个触发器配置。
|
||||
*
|
||||
* 该方法用于 RuleSceneForm 提交前兜底校验,避免子组件表单没有触发 validate 时漏掉必填项。
|
||||
* 校验逻辑需要和触发器主条件 UI 保持一致,同时继续校验附加条件组。
|
||||
*
|
||||
* @param trigger 触发器配置
|
||||
* @param index 触发器在列表中的下标,用于生成可定位的错误提示
|
||||
* @returns 错误信息,通过则返回 null
|
||||
*/
|
||||
export function validateTriggerItem(trigger: Trigger, index: number): string | null {
|
||||
if (!trigger.type) {
|
||||
return `触发器 ${index + 1}: 触发器类型不能为空`
|
||||
}
|
||||
|
||||
// 设备类触发器都有产品、设备两个基础字段。
|
||||
if (isDeviceTrigger(trigger.type)) {
|
||||
if (!trigger.productId) {
|
||||
return `触发器 ${index + 1}: 产品不能为空`
|
||||
}
|
||||
if (!trigger.deviceId) {
|
||||
return `触发器 ${index + 1}: 设备不能为空`
|
||||
}
|
||||
|
||||
// 设备状态变化不依赖物模型标识符,只校验操作符和状态值。
|
||||
if (trigger.type === IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE) {
|
||||
if (!trigger.operator) {
|
||||
return `触发器 ${index + 1}: 操作符不能为空`
|
||||
}
|
||||
if (isEmptyVal(trigger.value)) {
|
||||
return `触发器 ${index + 1}: 设备状态不能为空`
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
if (!trigger.identifier) {
|
||||
return `触发器 ${index + 1}: 物模型标识符不能为空`
|
||||
}
|
||||
|
||||
// 属性上报需要比较条件;事件上报、服务调用只需要物模型标识符。
|
||||
const isEventOrService =
|
||||
trigger.type === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST ||
|
||||
trigger.type === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
|
||||
if (!isEventOrService) {
|
||||
if (!trigger.operator) {
|
||||
return `触发器 ${index + 1}: 操作符不能为空`
|
||||
}
|
||||
if (isEmptyVal(trigger.value)) {
|
||||
return `触发器 ${index + 1}: 参数值不能为空`
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// 定时触发器本身需要 CRON 表达式,附加条件组在下方统一校验。
|
||||
if (trigger.type === IotRuleSceneTriggerTypeEnum.TIMER) {
|
||||
if (!trigger.cronExpression) {
|
||||
return `触发器 ${index + 1}: CRON表达式不能为空`
|
||||
}
|
||||
}
|
||||
|
||||
const groupError = validateTriggerConditionGroups(trigger.conditionGroups, index)
|
||||
if (groupError) {
|
||||
return groupError
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建附加子条件的表单校验规则。
|
||||
*
|
||||
* 该规则与 ConditionConfig 中不同条件类型展示的字段保持一致:
|
||||
* - 设备状态:需要产品、设备、操作符、设备状态;
|
||||
* - 设备属性:需要产品、设备、监控项、操作符、比较值;
|
||||
* - 当前时间:需要时间条件,并按时间条件动态校验时间值。
|
||||
*
|
||||
* @param conditionType 子条件类型
|
||||
* @param getOperator 获取当前时间条件操作符的方法,用于动态判断 param 是否必填
|
||||
* @returns Element Plus 表单 rules
|
||||
*/
|
||||
export function buildSubConditionRules(
|
||||
conditionType?: number,
|
||||
getOperator?: () => string
|
||||
): Record<string, FormItemRule[]> {
|
||||
const rules: Record<string, FormItemRule[]> = {
|
||||
type: [requiredRule('请选择条件类型')]
|
||||
}
|
||||
|
||||
if (!conditionType) {
|
||||
return rules
|
||||
}
|
||||
|
||||
// 设备状态和设备属性都需要先确定具体产品、设备。
|
||||
if (
|
||||
conditionType === IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS ||
|
||||
conditionType === IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY
|
||||
) {
|
||||
rules.productId = [requiredRule('请选择产品')]
|
||||
rules.deviceId = [requiredRule('请选择设备')]
|
||||
}
|
||||
|
||||
// 设备状态使用设备在线状态作为比较值。
|
||||
if (conditionType === IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS) {
|
||||
rules.operator = [requiredRule('请选择操作符')]
|
||||
rules.param = [requiredRule('请选择设备状态')]
|
||||
}
|
||||
|
||||
// 设备属性需要选择物模型属性,再填写对应的比较值。
|
||||
if (conditionType === IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY) {
|
||||
rules.identifier = [requiredRule('请选择监控项')]
|
||||
rules.operator = [requiredRule('请选择操作符')]
|
||||
rules.param = [requiredRule('请填写比较值')]
|
||||
}
|
||||
|
||||
// 当前时间的 param 是否必填取决于具体操作符,例如“今天”不需要额外值。
|
||||
if (conditionType === IotRuleSceneTriggerConditionTypeEnum.CURRENT_TIME) {
|
||||
rules.operator = [requiredRule('请选择时间条件')]
|
||||
rules.param = [createCurrentTimeParamRule(getOperator ?? (() => ''))]
|
||||
}
|
||||
|
||||
return rules
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建当前时间条件的 param 自定义校验规则。
|
||||
*
|
||||
* 时间条件的字段展示是动态的:
|
||||
* - 今天:不需要 param;
|
||||
* - 时间区间:param 使用逗号拼接开始时间和结束时间;
|
||||
* - 其它时间条件:param 需要填写单个时间值。
|
||||
*
|
||||
* @param getOperator 获取当前时间条件操作符的方法
|
||||
* @returns Element Plus 表单项校验规则
|
||||
*/
|
||||
function createCurrentTimeParamRule(getOperator: () => string): FormItemRule {
|
||||
return {
|
||||
validator: (_rule, value, callback) => {
|
||||
const operator = getOperator()
|
||||
|
||||
// “今天”没有附加输入项,直接通过。
|
||||
if (operator === IotRuleSceneTriggerTimeOperatorEnum.TODAY.value) {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
|
||||
if (isEmptyVal(value)) {
|
||||
callback(new Error('请填写时间值'))
|
||||
return
|
||||
}
|
||||
|
||||
// 时间区间需要同时存在开始和结束时间。
|
||||
if (operator === IotRuleSceneTriggerTimeOperatorEnum.BETWEEN_TIME.value) {
|
||||
const parts = String(value).split(',')
|
||||
if (!parts[0]?.trim() || !parts[1]?.trim()) {
|
||||
callback(new Error('请填写开始和结束时间'))
|
||||
return
|
||||
}
|
||||
}
|
||||
callback()
|
||||
},
|
||||
trigger: ['change', 'blur']
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验单个附加子条件。
|
||||
*
|
||||
* 该方法用于提交前兜底校验,错误提示会带上 path,方便定位到第几个触发器、
|
||||
* 第几个条件组和第几个条件。
|
||||
*
|
||||
* @param condition 子条件配置
|
||||
* @param path 错误提示前缀
|
||||
* @returns 错误信息,通过则返回 null
|
||||
*/
|
||||
export function validateTriggerCondition(
|
||||
condition: TriggerCondition,
|
||||
path: string
|
||||
): string | null {
|
||||
if (!condition.type) {
|
||||
return `${path}: 条件类型不能为空`
|
||||
}
|
||||
|
||||
// 设备状态和设备属性都必须先选择产品、设备。
|
||||
if (
|
||||
condition.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS ||
|
||||
condition.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY
|
||||
) {
|
||||
if (!condition.productId) {
|
||||
return `${path}: 产品不能为空`
|
||||
}
|
||||
if (!condition.deviceId) {
|
||||
return `${path}: 设备不能为空`
|
||||
}
|
||||
}
|
||||
|
||||
// 设备状态只校验操作符和状态枚举值。
|
||||
if (condition.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS) {
|
||||
if (!condition.operator) {
|
||||
return `${path}: 操作符不能为空`
|
||||
}
|
||||
if (isEmptyVal(condition.param)) {
|
||||
return `${path}: 设备状态不能为空`
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// 设备属性需要校验物模型标识符、操作符和比较值。
|
||||
if (condition.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY) {
|
||||
if (!condition.identifier) {
|
||||
return `${path}: 监控项不能为空`
|
||||
}
|
||||
if (!condition.operator) {
|
||||
return `${path}: 操作符不能为空`
|
||||
}
|
||||
if (isEmptyVal(condition.param)) {
|
||||
return `${path}: 比较值不能为空`
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// 当前时间按操作符动态判断 param 是否需要填写。
|
||||
if (condition.type === IotRuleSceneTriggerConditionTypeEnum.CURRENT_TIME) {
|
||||
if (!condition.operator) {
|
||||
return `${path}: 时间条件不能为空`
|
||||
}
|
||||
|
||||
if (condition.operator === IotRuleSceneTriggerTimeOperatorEnum.TODAY.value) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (isEmptyVal(condition.param)) {
|
||||
return `${path}: 时间值不能为空`
|
||||
}
|
||||
|
||||
if (condition.operator === IotRuleSceneTriggerTimeOperatorEnum.BETWEEN_TIME.value) {
|
||||
const parts = String(condition.param).split(',')
|
||||
if (!parts[0]?.trim() || !parts[1]?.trim()) {
|
||||
return `${path}: 开始和结束时间不能为空`
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验触发器的附加条件组。
|
||||
*
|
||||
* 条件组结构是二维数组:外层是 OR 关系的条件组,内层是 AND 关系的条件列表。
|
||||
* 这里逐组、逐条件返回第一条错误,避免一次性弹出过多提示。
|
||||
*
|
||||
* @param groups 附加条件组
|
||||
* @param triggerIndex 触发器在列表中的下标,用于生成可定位的错误提示
|
||||
* @returns 错误信息,通过则返回 null
|
||||
*/
|
||||
export function validateTriggerConditionGroups(
|
||||
groups: TriggerCondition[][] | undefined,
|
||||
triggerIndex: number
|
||||
): string | null {
|
||||
if (!groups?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
for (let g = 0; g < groups.length; g++) {
|
||||
const group = groups[g]
|
||||
|
||||
// 空条件组没有实际过滤条件,提交后语义不明确,需要拦截。
|
||||
if (!group?.length) {
|
||||
return `触发器 ${triggerIndex + 1} 子条件组 ${g + 1}: 至少需要一个条件`
|
||||
}
|
||||
|
||||
for (let c = 0; c < group.length; c++) {
|
||||
const error = validateTriggerCondition(
|
||||
group[c],
|
||||
`触发器 ${triggerIndex + 1} 子条件组 ${g + 1} 条件 ${c + 1}`
|
||||
)
|
||||
if (error) {
|
||||
return error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断执行器参数是否为空。
|
||||
*
|
||||
* params 当前以 JSON 字符串存储:
|
||||
* - 空字符串、空白字符串视为空;
|
||||
* - 空对象 `{}` 视为空;
|
||||
* - 非空 JSON 对象视为已配置;
|
||||
* - 非法 JSON 不在这里判空,交给 JSON 格式校验返回更准确的错误。
|
||||
*
|
||||
* @param params 执行器参数 JSON 字符串
|
||||
* @returns 是否为空
|
||||
*/
|
||||
export const isActionParamsEmpty = (params?: string): boolean => {
|
||||
if (!params || !String(params).trim()) {
|
||||
return true
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(String(params))
|
||||
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
||||
return Object.keys(parsed).length === 0
|
||||
}
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建设备控制执行器的表单校验规则。
|
||||
*
|
||||
* 属性设置和服务调用都需要产品、设备和参数配置;
|
||||
* 服务调用还需要选择具体服务标识符。
|
||||
*
|
||||
* @param actionType 执行器类型
|
||||
* @returns Element Plus 表单 rules
|
||||
*/
|
||||
export function buildDeviceControlRules(actionType: number): Record<string, FormItemRule[]> {
|
||||
const rules: Record<string, FormItemRule[]> = {
|
||||
productId: [requiredRule('请选择产品')],
|
||||
deviceId: [requiredRule('请选择设备')],
|
||||
params: [createParamsRule()]
|
||||
}
|
||||
|
||||
// 服务调用需要额外选择物模型服务。
|
||||
if (actionType === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE) {
|
||||
rules.identifier = [requiredRule('请选择服务')]
|
||||
}
|
||||
|
||||
return rules
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建告警恢复执行器的表单校验规则。
|
||||
*
|
||||
* 告警恢复需要绑定一个告警配置,后端据此定位需要恢复的告警类型。
|
||||
*
|
||||
* @returns Element Plus 表单 rules
|
||||
*/
|
||||
export function buildAlertConfigRules(): Record<string, FormItemRule[]> {
|
||||
return {
|
||||
alertConfigId: [requiredRule('请选择告警配置')]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建设备控制参数的自定义校验规则。
|
||||
*
|
||||
* 参数配置来自物模型属性或服务参数,提交前需要同时满足:
|
||||
* - 已经填写了参数;
|
||||
* - 参数字符串是合法 JSON。
|
||||
*
|
||||
* @returns Element Plus 表单项校验规则
|
||||
*/
|
||||
function createParamsRule(): FormItemRule {
|
||||
return {
|
||||
validator: (_rule, value, callback) => {
|
||||
if (isActionParamsEmpty(value)) {
|
||||
callback(new Error('请填写参数配置'))
|
||||
return
|
||||
}
|
||||
|
||||
// 这里只校验 JSON 语法,具体参数结构由物模型参数配置组件负责生成。
|
||||
try {
|
||||
JSON.parse(String(value))
|
||||
} catch {
|
||||
callback(new Error('参数格式须为合法 JSON'))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
},
|
||||
trigger: ['change', 'blur']
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验单个执行器配置。
|
||||
*
|
||||
* 该方法用于 RuleSceneForm 提交前兜底校验,避免子组件表单没有触发 validate 时漏掉必填项。
|
||||
* 校验逻辑需要和执行器 UI 保持一致。
|
||||
*
|
||||
* @param action 执行器配置
|
||||
* @param index 执行器在列表中的下标,用于生成可定位的错误提示
|
||||
* @returns 错误信息,通过则返回 null
|
||||
*/
|
||||
export function validateActionItem(action: Action, index: number): string | null {
|
||||
const prefix = `执行器 ${index + 1}`
|
||||
|
||||
if (!action.type) {
|
||||
return `${prefix}: 执行器类型不能为空`
|
||||
}
|
||||
|
||||
// 设备属性设置和设备服务调用都需要指定设备,并填写物模型参数。
|
||||
if (
|
||||
action.type === IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET ||
|
||||
action.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE
|
||||
) {
|
||||
if (!action.productId) {
|
||||
return `${prefix}: 产品不能为空`
|
||||
}
|
||||
if (!action.deviceId) {
|
||||
return `${prefix}: 设备不能为空`
|
||||
}
|
||||
if (action.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE && !action.identifier) {
|
||||
return `${prefix}: 服务不能为空`
|
||||
}
|
||||
|
||||
if (isActionParamsEmpty(action.params)) {
|
||||
return `${prefix}: 参数配置不能为空`
|
||||
}
|
||||
|
||||
try {
|
||||
JSON.parse(String(action.params))
|
||||
} catch {
|
||||
return `${prefix}: 参数格式须为合法 JSON`
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// 告警恢复执行器需要绑定具体告警配置。
|
||||
if (action.type === IotRuleSceneActionTypeEnum.ALERT_RECOVER) {
|
||||
if (!action.alertConfigId) {
|
||||
return `${prefix}: 告警配置不能为空`
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
|
@ -10,6 +10,9 @@
|
|||
<el-form-item label="手机号" prop="mobile">
|
||||
<el-input v-model="formData.mobile" placeholder="请输入手机号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model="formData.email" maxlength="50" placeholder="请输入邮箱" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio
|
||||
|
|
@ -91,6 +94,7 @@ const formType = ref('') // 表单的类型:create - 新增;update - 修改
|
|||
const formData = ref({
|
||||
id: undefined,
|
||||
mobile: undefined,
|
||||
email: undefined,
|
||||
password: undefined,
|
||||
status: undefined,
|
||||
nickname: undefined,
|
||||
|
|
@ -105,6 +109,7 @@ const formData = ref({
|
|||
})
|
||||
const formRules = reactive({
|
||||
mobile: [{ required: true, message: '手机号不能为空', trigger: 'blur' }],
|
||||
email: [{ type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change'] }],
|
||||
status: [{ required: true, message: '状态不能为空', trigger: 'blur' }]
|
||||
})
|
||||
const formRef = ref() // 表单 Ref
|
||||
|
|
@ -162,6 +167,7 @@ const resetForm = () => {
|
|||
formData.value = {
|
||||
id: undefined,
|
||||
mobile: undefined,
|
||||
email: undefined,
|
||||
password: undefined,
|
||||
status: undefined,
|
||||
nickname: undefined,
|
||||
|
|
|
|||
|
|
@ -27,6 +27,12 @@
|
|||
</template>
|
||||
{{ user.mobile }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item>
|
||||
<template #label>
|
||||
<descriptions-item-label icon="ep:message" label="邮箱" />
|
||||
</template>
|
||||
{{ user.email || '空' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item>
|
||||
<template #label>
|
||||
<descriptions-item-label icon="fa:mars-double" label="性别" />
|
||||
|
|
@ -87,6 +93,12 @@
|
|||
</template>
|
||||
{{ user.mobile }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item>
|
||||
<template #label>
|
||||
<descriptions-item-label icon="ep:message" label="邮箱" />
|
||||
</template>
|
||||
{{ user.email || '空' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item>
|
||||
<template #label>
|
||||
<descriptions-item-label icon="fa:mars-double" label="性别" />
|
||||
|
|
|
|||
|
|
@ -28,6 +28,15 @@
|
|||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input
|
||||
v-model="queryParams.email"
|
||||
class="!w-240px"
|
||||
clearable
|
||||
placeholder="请输入邮箱"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="注册时间" prop="createTime">
|
||||
<el-date-picker
|
||||
v-model="queryParams.createTime"
|
||||
|
|
@ -90,6 +99,7 @@
|
|||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="手机号" prop="mobile" width="120px" />
|
||||
<el-table-column align="center" label="邮箱" prop="email" width="180px" />
|
||||
<el-table-column align="center" label="昵称" prop="nickname" width="80px" />
|
||||
<el-table-column align="center" label="等级" prop="levelName" width="100px" />
|
||||
<el-table-column align="center" label="分组" prop="groupName" width="100px" />
|
||||
|
|
@ -227,6 +237,7 @@ const queryParams = reactive({
|
|||
pageSize: 10,
|
||||
nickname: null,
|
||||
mobile: null,
|
||||
email: null,
|
||||
loginDate: [],
|
||||
createTime: [],
|
||||
tagIds: [],
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
v-model="scope.row.handlerRoleId"
|
||||
placeholder="请选择角色"
|
||||
/>
|
||||
<RoleSelect v-else :model-value="scope.row.handlerRoleId" disabled />
|
||||
<span v-else>{{ scope.row.handlerRoleName || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="处置人" align="center" width="180">
|
||||
|
|
|
|||
|
|
@ -19,7 +19,9 @@
|
|||
:disabled="isHeaderReadonly"
|
||||
>
|
||||
<template #append>
|
||||
<el-button @click="generateCode"> 生成 </el-button>
|
||||
<el-button :disabled="isHeaderReadonly" @click="generateCode">
|
||||
生成
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
:disabled="isHeaderReadonly"
|
||||
>
|
||||
<el-option
|
||||
v-for="dict in getStrDictOptions(DICT_TYPE.MES_WM_MISC_ISSUE_TYPE)"
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.MES_WM_MISC_ISSUE_TYPE)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
|
|
@ -120,7 +120,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
|
||||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||
import { WmMiscIssueApi, WmMiscIssueVO } from '@/api/mes/wm/miscissue'
|
||||
import { AutoCodeRecordApi } from '@/api/mes/md/autocode/record'
|
||||
import MiscIssueLineList from './MiscIssueLineList.vue'
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@
|
|||
class="!w-240px"
|
||||
>
|
||||
<el-option
|
||||
v-for="dict in getStrDictOptions(DICT_TYPE.MES_WM_MISC_ISSUE_TYPE)"
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.MES_WM_MISC_ISSUE_TYPE)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
|
|
@ -195,7 +195,7 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { dateFormatter2 } from '@/utils/formatTime'
|
||||
import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
|
||||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||
import download from '@/utils/download'
|
||||
import { WmMiscIssueApi, WmMiscIssueVO } from '@/api/mes/wm/miscissue'
|
||||
import MiscIssueForm from './MiscIssueForm.vue'
|
||||
|
|
|
|||
|
|
@ -118,13 +118,12 @@
|
|||
</template>
|
||||
<template v-else-if="formData.type === MesWmStockTakingParamTypeEnum.QUALITY_STATUS">
|
||||
<el-select
|
||||
v-model="formData.valueCode"
|
||||
v-model="qualityStatusValue"
|
||||
placeholder="请选择质量状态"
|
||||
class="!w-full"
|
||||
@change="handleQualityStatusChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="dict in getStrDictOptions(DICT_TYPE.MES_WM_QUALITY_STATUS)"
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.MES_WM_QUALITY_STATUS)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
|
|
@ -148,7 +147,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
|
||||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||
import {
|
||||
StockTakingPlanParamApi,
|
||||
type StockTakingPlanParamVO
|
||||
|
|
@ -219,6 +218,10 @@ const formData = ref<StockTakingPlanParamVO>({
|
|||
valueName: '',
|
||||
remark: ''
|
||||
})
|
||||
const qualityStatusValue = computed({
|
||||
get: () => (formData.value.valueCode ? Number(formData.value.valueCode) : undefined),
|
||||
set: (val?: number) => handleQualityStatusChange(val)
|
||||
})
|
||||
const formRules = reactive({
|
||||
type: [{ required: true, message: '请选择条件类型', trigger: 'change' }],
|
||||
valueId: [
|
||||
|
|
@ -331,11 +334,11 @@ const handleBatchChange = (batch?: any) => {
|
|||
}
|
||||
|
||||
/** 质量状态选择器变化 */
|
||||
const handleQualityStatusChange = (val: string) => {
|
||||
const dictOptions = getStrDictOptions(DICT_TYPE.MES_WM_QUALITY_STATUS)
|
||||
const handleQualityStatusChange = (val?: number) => {
|
||||
const dictOptions = getIntDictOptions(DICT_TYPE.MES_WM_QUALITY_STATUS)
|
||||
const selected = dictOptions.find((d) => d.value === val)
|
||||
formData.value.valueId = undefined // 质量状态无实体 ID
|
||||
formData.value.valueCode = val
|
||||
formData.value.valueCode = val == null ? '' : String(val)
|
||||
formData.value.valueName = selected?.label || ''
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
<template>
|
||||
<el-select
|
||||
:model-value="modelValue"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:loading="loading"
|
||||
class="w-full"
|
||||
filterable
|
||||
clearable
|
||||
@update:model-value="handleChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="template in templateList"
|
||||
:key="template.id"
|
||||
:label="`${template.name}(${template.code})`"
|
||||
:value="template.code"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
getSimpleMailTemplateList,
|
||||
type MailTemplateSimpleVO
|
||||
} from '@/api/system/mail/template'
|
||||
|
||||
/** 邮件模板下拉选择器 */
|
||||
defineOptions({ name: 'MailTemplateSelect' })
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
disabled?: boolean
|
||||
modelValue?: string
|
||||
placeholder?: string
|
||||
}>(),
|
||||
{
|
||||
disabled: false,
|
||||
modelValue: undefined,
|
||||
placeholder: '请选择邮件模板'
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value?: string): void
|
||||
(e: 'change', value?: string): void
|
||||
}>()
|
||||
|
||||
const loading = ref(false)
|
||||
const templateList = ref<MailTemplateSimpleVO[]>([])
|
||||
|
||||
const handleChange = (value?: string) => {
|
||||
emit('update:modelValue', value)
|
||||
emit('change', value)
|
||||
}
|
||||
|
||||
const getTemplateList = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
templateList.value = (await getSimpleMailTemplateList()) || []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getTemplateList()
|
||||
})
|
||||
</script>
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
<template>
|
||||
<el-select
|
||||
:model-value="modelValue"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:loading="loading"
|
||||
class="w-full"
|
||||
filterable
|
||||
clearable
|
||||
@update:model-value="handleChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="template in templateList"
|
||||
:key="template.id"
|
||||
:label="`${template.name}(${template.code})`"
|
||||
:value="template.code"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
getSimpleNotifyTemplateList,
|
||||
type NotifyTemplateSimpleVO
|
||||
} from '@/api/system/notify/template'
|
||||
|
||||
/** 站内信模板下拉选择器 */
|
||||
defineOptions({ name: 'NotifyTemplateSelect' })
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
disabled?: boolean
|
||||
modelValue?: string
|
||||
placeholder?: string
|
||||
}>(),
|
||||
{
|
||||
disabled: false,
|
||||
modelValue: undefined,
|
||||
placeholder: '请选择站内信模板'
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value?: string): void
|
||||
(e: 'change', value?: string): void
|
||||
}>()
|
||||
|
||||
const loading = ref(false)
|
||||
const templateList = ref<NotifyTemplateSimpleVO[]>([])
|
||||
|
||||
const handleChange = (value?: string) => {
|
||||
emit('update:modelValue', value)
|
||||
emit('change', value)
|
||||
}
|
||||
|
||||
const getTemplateList = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
templateList.value = (await getSimpleNotifyTemplateList()) || []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getTemplateList()
|
||||
})
|
||||
</script>
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
<template>
|
||||
<el-select
|
||||
:model-value="modelValue"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:loading="loading"
|
||||
class="w-full"
|
||||
filterable
|
||||
clearable
|
||||
@update:model-value="handleChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="template in templateList"
|
||||
:key="template.id"
|
||||
:label="`${template.name}(${template.code})`"
|
||||
:value="template.code"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
getSimpleSmsTemplateList,
|
||||
type SmsTemplateSimpleVO
|
||||
} from '@/api/system/sms/smsTemplate'
|
||||
|
||||
/** 短信模板下拉选择器 */
|
||||
defineOptions({ name: 'SmsTemplateSelect' })
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
disabled?: boolean
|
||||
modelValue?: string
|
||||
placeholder?: string
|
||||
}>(),
|
||||
{
|
||||
disabled: false,
|
||||
modelValue: undefined,
|
||||
placeholder: '请选择短信模板'
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value?: string): void
|
||||
(e: 'change', value?: string): void
|
||||
}>()
|
||||
|
||||
const loading = ref(false)
|
||||
const templateList = ref<SmsTemplateSimpleVO[]>([])
|
||||
|
||||
const handleChange = (value?: string) => {
|
||||
emit('update:modelValue', value)
|
||||
emit('change', value)
|
||||
}
|
||||
|
||||
const getTemplateList = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
templateList.value = (await getSimpleSmsTemplateList()) || []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getTemplateList()
|
||||
})
|
||||
</script>
|
||||
Loading…
Reference in New Issue