Merge remote-tracking branch 'origin/master'

pull/881/head v2026.05
YunaiV 2026-05-31 21:51:09 +08:00
commit 37c70daaaf
40 changed files with 1947 additions and 363 deletions

View File

@ -10,6 +10,9 @@ export interface AlertConfig {
sceneRuleIds: string // 关联的场景联动规则编号数组 sceneRuleIds: string // 关联的场景联动规则编号数组
receiveUserIds: string // 接收的用户编号数组 receiveUserIds: string // 接收的用户编号数组
receiveTypes: string // 接收的类型数组 receiveTypes: string // 接收的类型数组
smsTemplateCode?: string // 短信模板编号
mailTemplateCode?: string // 邮件模板编号
notifyTemplateCode?: string // 站内信模板编号
} }
// IoT 告警配置 API // IoT 告警配置 API

View File

@ -215,8 +215,8 @@ export const ThingModelFormRules = {
identifier: [ identifier: [
{ required: true, message: '标识符不能为空', trigger: 'blur' }, { required: true, message: '标识符不能为空', trigger: 'blur' },
{ {
pattern: /^[a-zA-Z0-9_]{1,50}$/, pattern: /^[a-zA-Z][a-zA-Z0-9_]{0,31}$/,
message: '支持大小写字母、数字和下划线,不超过 50 个字符', message: '支持大小写字母、数字和下划线,必须以字母开头,不超过 32 个字符',
trigger: 'blur' trigger: 'blur'
}, },
{ {

View File

@ -9,6 +9,7 @@ export interface UserVO {
loginIp: string loginIp: string
mark: string mark: string
mobile: string mobile: string
email: string | undefined
name: string | undefined name: string | undefined
nickname: string | undefined nickname: string | undefined
registerIp: string registerIp: string

View File

@ -6,6 +6,7 @@ export interface ProAndonConfigVO {
reason: string // 呼叫原因 reason: string // 呼叫原因
level: number // 级别 level: number // 级别
handlerRoleId: number // 处置人角色编号 handlerRoleId: number // 处置人角色编号
handlerRoleName: string // 处置人角色名称
handlerUserId: number // 处置人编号 handlerUserId: number // 处置人编号
handlerUserNickname: string // 处置人昵称 handlerUserNickname: string // 处置人昵称
remark: string // 备注 remark: string // 备注

View File

@ -5,7 +5,7 @@ export interface WmMiscIssueVO {
id: number id: number
code: string code: string
name: string name: string
type: string type: number
sourceDocType: string sourceDocType: string
sourceDocId: number sourceDocId: number
sourceDocCode: string sourceDocCode: string

View File

@ -19,6 +19,16 @@ export interface MailSendReqVO {
templateParams: Map<String, Object> 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) => { export const getMailTemplatePage = async (params: PageParam) => {
return await request.get({ url: '/system/mail-template/page', params }) return await request.get({ url: '/system/mail-template/page', params })

View File

@ -18,6 +18,17 @@ export interface NotifySendReqVO {
templateParams: Map<String, Object> 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) => { export const getNotifyTemplatePage = async (params: PageParam) => {
return await request.get({ url: '/system/notify-template/page', params }) 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[]) => { 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(',') }
})
} }
// 发送站内信 // 发送站内信

View File

@ -21,6 +21,16 @@ export interface SendSmsReqVO {
templateParams: Map<String, Object> 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) => { export const getSmsTemplatePage = (params: PageParam) => {
return request.get({ url: '/system/sms-template/page', params }) return request.get({ url: '/system/sms-template/page', params })

View File

@ -1,6 +1,12 @@
<!-- 数据字典 Select 选择器 --> <!-- 数据字典 Select 选择器 -->
<template> <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 <el-option
v-for="(dict, index) in getDictOptions" v-for="(dict, index) in getDictOptions"
:key="index" :key="index"
@ -8,12 +14,24 @@
:value="dict.value" :value="dict.value"
/> />
</el-select> </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"> <el-radio v-for="(dict, index) in getDictOptions" :key="index" :value="dict.value">
{{ dict.label }} {{ dict.label }}
</el-radio> </el-radio>
</el-radio-group> </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 <el-checkbox
v-for="(dict, index) in getDictOptions" v-for="(dict, index) in getDictOptions"
:key="index" :key="index"
@ -33,6 +51,7 @@ const attrs = useAttrs()
// //
interface Props { interface Props {
dictType: string // dictType: string //
modelValue?: any // form-create modelValue
valueType?: 'str' | 'int' | 'bool' // valueType?: 'str' | 'int' | 'bool' //
selectType?: 'select' | 'radio' | 'checkbox' // select checkbox radio selectType?: 'select' | 'radio' | 'checkbox' // select checkbox radio
formCreateInject?: any formCreateInject?: any
@ -43,6 +62,20 @@ const props = withDefaults(defineProps<Props>(), {
selectType: 'select' 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(() => { const getDictOptions = computed(() => {
switch (props.valueType) { switch (props.valueType) {
@ -56,4 +89,8 @@ const getDictOptions = computed(() => {
return [] return []
} }
}) })
const handleChange = (value: any) => {
emit('update:modelValue', value)
}
</script> </script>

View File

@ -1,10 +1,53 @@
<script setup lang="ts"> <script setup lang="ts">
import * as ProcessInstanceApi from '@/api/bpm/processInstance' 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 { useUserStore } from '@/store/modules/user'
import { formatDate } from '@/utils/formatTime' 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' 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 userStore = useUserStore()
const visible = ref(false) const visible = ref(false)
@ -13,15 +56,16 @@ const loading = ref(false)
const printData = ref() const printData = ref()
const userName = computed(() => userStore.user.nickname ?? '') const userName = computed(() => userStore.user.nickname ?? '')
const printTime = ref(formatDate(new Date(), 'YYYY-MM-DD HH:mm')) const printTime = ref(formatDate(new Date(), 'YYYY-MM-DD HH:mm'))
const formFields = ref() const formFields = ref<FormFieldItem[]>([])
const printDataMap = ref({}) const printDataMap = ref<Record<string, string>>({})
const open = async (id: string) => { const open = async (id: string) => {
loading.value = true loading.value = true
try { try {
printData.value = await ProcessInstanceApi.getProcessInstancePrintData(id) printData.value = await ProcessInstanceApi.getProcessInstancePrintData(id)
printTime.value = formatDate(new Date(), 'YYYY-MM-DD HH:mm')
initPrintDataMap() initPrintDataMap()
parseFormFields() await parseFormFields()
} finally { } finally {
loading.value = false loading.value = false
} }
@ -29,82 +73,347 @@ const open = async (id: string) => {
} }
defineExpose({ open }) defineExpose({ open })
const parseFormFields = () => { const parseFormFields = async () => {
if (!printData.value) return if (!printData.value) return
const formFieldsObj = decodeFields( const formFieldsObj = decodeFields(
printData.value.processInstance.processDefinition?.formFields || [] printData.value.processInstance.processDefinition?.formFields || []
) ) as FormFieldRule[]
const processVariables = printData.value.processInstance.formVariables const processVariables = printData.value.processInstance.formVariables ?? {}
let res: any = [] const lookupMaps = await loadPrintLookupMaps(formFieldsObj)
const res: FormFieldItem[] = []
for (const item of formFieldsObj) { for (const item of formFieldsObj) {
const id = item['field'] const fieldKey = String(item.field ?? '')
const name = item['title'] const id = fieldKey
const variable = processVariables[item['field']] const name = String(item.title ?? fieldKey)
let html = variable const variable = processVariables[fieldKey]
switch (item['type']) { const html = formatPrintField(item, variable, lookupMaps)
case 'UploadImg': {
let imgEl = document.createElement('img') printDataMap.value[fieldKey] = html
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
res.push({ id, name, html }) res.push({ id, name, html })
} }
formFields.value = res 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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
}
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 = () => { const initPrintDataMap = () => {
printDataMap.value['startUser'] = printData.value.processInstance.startUser.nickname if (!printData.value) return
printDataMap.value['startUserDept'] = printData.value.processInstance.startUser.deptName
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['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['startTime'] = formatDate(printData.value.processInstance.startTime)
printDataMap.value['endTime'] = formatDate(printData.value.processInstance.endTime) printDataMap.value['endTime'] = formatDate(printData.value.processInstance.endTime)
printDataMap.value['processStatus'] = getDictLabel( printDataMap.value['processStatus'] = String(getDictLabel(
DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS, DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS,
printData.value.processInstance.status printData.value.processInstance.status
) ) ?? '')
printDataMap.value['printUser'] = userName.value printDataMap.value['printUser'] = userName.value
printDataMap.value['printTime'] = printTime.value printDataMap.value['printTime'] = printTime.value
} }
const getPrintTemplateHTML = () => { const getPrintTemplateHTML = () => {
if (!printData.value?.printTemplateHtml) return ''
const parser = new DOMParser() const parser = new DOMParser()
let doc = parser.parseFromString(printData.value.printTemplateHtml, 'text/html') const doc = parser.parseFromString(printData.value.printTemplateHtml, 'text/html')
// table border // table border
let tables = doc.querySelectorAll('table') const tables = doc.querySelectorAll('table')
tables.forEach((item) => { tables.forEach((item) => {
item.setAttribute('border', '1') item.setAttribute('border', '1')
item.setAttribute('style', (item.getAttribute('style') || '') + 'border-collapse:collapse;') item.setAttribute('style', (item.getAttribute('style') || '') + 'border-collapse:collapse;')
}) })
// mentions // mentions
let mentions = doc.querySelectorAll('[data-w-e-type="mention"]') const mentions = doc.querySelectorAll('[data-w-e-type="mention"]')
mentions.forEach((item) => { mentions.forEach((item) => {
const mentionId = JSON.parse(decodeURIComponent(item.getAttribute('data-info') ?? ''))['id'] const mentionId = JSON.parse(decodeURIComponent(item.getAttribute('data-info') ?? ''))['id']
item.innerHTML = printDataMap.value[mentionId] ?? '' item.innerHTML = printDataMap.value[mentionId] ?? ''
}) })
// //
let processRecords = doc.querySelectorAll('[data-w-e-type="process-record"]') const processRecords = doc.querySelectorAll('[data-w-e-type="process-record"]')
let processRecordTable: Element = document.createElement('table') const processRecordTable: Element = document.createElement('table')
if (processRecords.length > 0) { if (processRecords.length > 0) {
// html // html
processRecordTable.setAttribute('border', '1') processRecordTable.setAttribute('border', '1')
@ -114,15 +423,15 @@ const getPrintTemplateHTML = () => {
headTd.setAttribute('colspan', '2') headTd.setAttribute('colspan', '2')
headTd.setAttribute('width', 'auto') headTd.setAttribute('width', 'auto')
headTd.setAttribute('style', 'text-align: center;') headTd.setAttribute('style', 'text-align: center;')
headTd.innerHTML = '流程节点' headTd.textContent = '流程节点'
headTr.appendChild(headTd) headTr.appendChild(headTd)
processRecordTable.appendChild(headTr) processRecordTable.appendChild(headTr)
printData.value.tasks.forEach((item) => { printData.value.tasks.forEach((item) => {
const tr = document.createElement('tr') const tr = document.createElement('tr')
const td1 = document.createElement('td') const td1 = document.createElement('td')
td1.innerHTML = item.name td1.textContent = item.name
const td2 = document.createElement('td') const td2 = document.createElement('td')
td2.innerHTML = item.description td2.textContent = item.description
tr.appendChild(td1) tr.appendChild(td1)
tr.appendChild(td2) tr.appendChild(td2)
processRecordTable.appendChild(tr) processRecordTable.appendChild(tr)

View File

@ -44,7 +44,7 @@
<el-tab-pane label="审批详情" name="form"> <el-tab-pane label="审批详情" name="form">
<div class="form-scroll-area"> <div class="form-scroll-area">
<el-scrollbar> <el-scrollbar>
<el-row> <el-row :gutter="40">
<el-col :span="17" class="!flex !flex-col formCol"> <el-col :span="17" class="!flex !flex-col formCol">
<!-- 表单信息 --> <!-- 表单信息 -->
<div <div
@ -52,14 +52,14 @@
class="form-box flex flex-col mb-30px flex-1" 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 <form-create
v-model="detailForm.value" v-model="detailForm.value"
v-model:api="fApi" v-model:api="fApi"
:option="detailForm.option" :option="detailForm.option"
:rule="detailForm.rule" :rule="detailForm.rule"
/> />
</el-col> </div>
<!-- 情况二业务表单 --> <!-- 情况二业务表单 -->
<div v-if="processDefinition?.formType === BpmModelFormType.CUSTOM"> <div v-if="processDefinition?.formType === BpmModelFormType.CUSTOM">
<BusinessFormComponent :id="processInstance.businessKey" /> <BusinessFormComponent :id="processInstance.businessKey" />

View File

@ -112,6 +112,7 @@ const list = ref([]) // 列表的数据
const queryParams = reactive({ const queryParams = reactive({
pageNo: 1, pageNo: 1,
pageSize: 10, pageSize: 10,
sceneType: 1,
followUpStatus: false, followUpStatus: false,
transformStatus: false transformStatus: false
}) })

View File

@ -11,12 +11,7 @@
<el-input v-model="formData.name" placeholder="请输入配置名称" /> <el-input v-model="formData.name" placeholder="请输入配置名称" />
</el-form-item> </el-form-item>
<el-form-item label="配置描述" prop="description"> <el-form-item label="配置描述" prop="description">
<el-input <el-input v-model="formData.description" placeholder="请输入配置描述" />
v-model="formData.description"
type="textarea"
:rows="3"
placeholder="请输入配置描述"
/>
</el-form-item> </el-form-item>
<el-form-item label="告警级别" prop="level"> <el-form-item label="告警级别" prop="level">
<el-select v-model="formData.level" placeholder="请选择告警级别"> <el-select v-model="formData.level" placeholder="请选择告警级别">
@ -83,6 +78,27 @@
/> />
</el-select> </el-select>
</el-form-item> </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> </el-form>
<template #footer> <template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button> <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 { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { CommonStatusEnum } from '@/utils/constants' import { CommonStatusEnum } from '@/utils/constants'
import { RuleSceneApi } from '@/api/iot/rule/scene' import { RuleSceneApi } from '@/api/iot/rule/scene'
import { IotAlertReceiveTypeEnum } from '@/views/iot/utils/constants'
import * as UserApi from '@/api/system/user' 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 告警配置 表单 */ /** IoT 告警配置 表单 */
defineOptions({ name: 'AlertConfigForm' }) defineOptions({ name: 'AlertConfigForm' })
@ -107,7 +127,19 @@ const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // const dialogTitle = ref('') //
const formLoading = ref(false) // 12 const formLoading = ref(false) // 12
const formType = ref('') // create - update - 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, id: undefined,
name: undefined, name: undefined,
description: undefined, description: undefined,
@ -115,9 +147,12 @@ const formData = ref({
status: CommonStatusEnum.ENABLE, status: CommonStatusEnum.ENABLE,
sceneRuleIds: [], sceneRuleIds: [],
receiveUserIds: [], receiveUserIds: [],
receiveTypes: [] receiveTypes: [],
smsTemplateCode: undefined,
mailTemplateCode: undefined,
notifyTemplateCode: undefined
}) })
const formRules = reactive({ const formRules = reactive<Record<string, any>>({
name: [{ required: true, message: '配置名称不能为空', trigger: 'blur' }], name: [{ required: true, message: '配置名称不能为空', trigger: 'blur' }],
level: [{ required: true, message: '告警级别不能为空', trigger: 'blur' }], level: [{ required: true, message: '告警级别不能为空', trigger: 'blur' }],
status: [{ required: true, message: '配置状态不能为空', trigger: 'blur' }], status: [{ required: true, message: '配置状态不能为空', trigger: 'blur' }],
@ -131,6 +166,47 @@ const formRef = ref() // 表单 Ref
const sceneRuleOptions = ref<any[]>([]) const sceneRuleOptions = ref<any[]>([])
const userOptions = ref<UserApi.UserVO[]>([]) 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) => { const open = async (type: string, id?: number) => {
dialogVisible.value = true dialogVisible.value = true
@ -143,6 +219,7 @@ const open = async (type: string, id?: number) => {
formLoading.value = true formLoading.value = true
try { try {
formData.value = await AlertConfigApi.getAlertConfig(id) formData.value = await AlertConfigApi.getAlertConfig(id)
syncTemplateFormRules()
} finally { } finally {
formLoading.value = false formLoading.value = false
} }
@ -156,10 +233,12 @@ defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 加载选项数据 */ /** 加载选项数据 */
const loadOptions = async () => { const loadOptions = async () => {
try { try {
// const [scenes, users] = await Promise.all([
sceneRuleOptions.value = await RuleSceneApi.getSimpleRuleSceneList() RuleSceneApi.getSimpleRuleSceneList(),
// UserApi.getSimpleUserList()
userOptions.value = await UserApi.getSimpleUserList() ])
sceneRuleOptions.value = scenes
userOptions.value = users
} catch (error) { } catch (error) {
console.error('加载选项数据失败:', error) console.error('加载选项数据失败:', error)
} }
@ -199,8 +278,12 @@ const resetForm = () => {
status: CommonStatusEnum.ENABLE, status: CommonStatusEnum.ENABLE,
sceneRuleIds: [], sceneRuleIds: [],
receiveUserIds: [], receiveUserIds: [],
receiveTypes: [] receiveTypes: [],
smsTemplateCode: undefined,
mailTemplateCode: undefined,
notifyTemplateCode: undefined
} }
syncTemplateFormRules()
formRef.value?.resetFields() formRef.value?.resetFields()
} }
</script> </script>

View File

@ -308,9 +308,9 @@
</template> </template>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="设备状态" align="center" prop="status"> <el-table-column label="设备状态" align="center" prop="state">
<template #default="scope"> <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> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column

View File

@ -56,21 +56,28 @@ watch([urlPrefix, urlPath], () => {
config.value.url = fullUrl.value 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(() => { onMounted(() => {
if (!isEmpty(config.value)) { if (!isEmpty(config.value)) {
// URL syncUrlFields(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
}
}
return return
} }

View File

@ -4,7 +4,7 @@
:title="drawerTitle" :title="drawerTitle"
size="80%" size="80%"
direction="rtl" direction="rtl"
:close-on-click-modal="false" :close-on-click-modal="true"
:close-on-press-escape="false" :close-on-press-escape="false"
@close="handleClose" @close="handleClose"
> >
@ -12,9 +12,9 @@
<!-- 基础信息配置 --> <!-- 基础信息配置 -->
<BasicInfoSection v-model="formData" :rules="formRules" /> <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> </el-form>
<template #footer> <template #footer>
<div class="drawer-footer"> <div class="drawer-footer">
@ -38,11 +38,9 @@ import TriggerSection from './sections/TriggerSection.vue'
import ActionSection from './sections/ActionSection.vue' import ActionSection from './sections/ActionSection.vue'
import { IotSceneRule } from '@/api/iot/rule/scene' import { IotSceneRule } from '@/api/iot/rule/scene'
import { RuleSceneApi } from '@/api/iot/rule/scene' import { RuleSceneApi } from '@/api/iot/rule/scene'
import { import { IotRuleSceneTriggerTypeEnum } from '@/views/iot/utils/constants'
IotRuleSceneTriggerTypeEnum, import { validateActionItem, validateTriggerItem } from '@/views/iot/utils/sceneRule'
IotRuleSceneActionTypeEnum, import type { Trigger, Action } from '@/api/iot/rule/scene'
isDeviceTrigger
} from '@/views/iot/utils/constants'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { CommonStatusEnum } from '@/utils/constants' import { CommonStatusEnum } from '@/utils/constants'
@ -91,66 +89,34 @@ const createDefaultFormData = (): IotSceneRule => {
} }
const formRef = ref() // const formRef = ref() //
const triggerSectionRef = ref<{
validateAllTriggers: () => Promise<boolean>
clearAllTriggerValidate: () => void
}>()
const actionSectionRef = ref<{
validateAllActions: () => Promise<boolean>
clearAllActionValidate: () => void
}>()
const formData = ref<IotSceneRule>(createDefaultFormData()) // const formData = ref<IotSceneRule>(createDefaultFormData()) //
/** /**
* 触发器校验器 * 触发器校验器兜底与主条件 UI 规则一致
* @param _rule 校验规则未使用 * @param _rule 校验规则未使用
* @param value 校验值 * @param value 校验值
* @param callback 回调函数 * @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) { if (!value || !Array.isArray(value) || value.length === 0) {
callback(new Error('至少需要一个触发器')) callback(new Error('至少需要一个触发器'))
return return
} }
for (let i = 0; i < value.length; i++) { for (let i = 0; i < value.length; i++) {
const trigger = value[i] const error = validateTriggerItem(value[i], i)
if (error) {
// callback(new Error(error))
if (!trigger.type) {
callback(new Error(`触发器 ${i + 1}: 触发器类型不能为空`))
return 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() callback()
@ -162,59 +128,18 @@ const validateTriggers = (_rule: any, value: any, callback: any) => {
* @param value 校验值 * @param value 校验值
* @param callback 回调函数 * @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) { if (!value || !Array.isArray(value) || value.length === 0) {
callback(new Error('至少需要一个执行器')) callback(new Error('至少需要一个执行器'))
return return
} }
for (let i = 0; i < value.length; i++) { for (let i = 0; i < value.length; i++) {
const action = value[i] const error = validateActionItem(value[i], i)
if (error) {
// callback(new Error(error))
if (!action.type) {
callback(new Error(`执行器 ${i + 1}: 执行器类型不能为空`))
return 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() callback()
@ -247,10 +172,22 @@ const drawerTitle = computed(() => (isEdit.value ? '编辑场景联动规则' :
/** 提交表单 */ /** 提交表单 */
const handleSubmit = async () => { const handleSubmit = async () => {
//
if (!formRef.value) return if (!formRef.value) return
const valid = await formRef.value.validate() try {
if (!valid) return 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 submitLoading.value = true
@ -320,6 +257,8 @@ watch(drawerVisible, async (visible) => {
// //
await nextTick() await nextTick()
formRef.value?.clearValidate() formRef.value?.clearValidate()
triggerSectionRef.value?.clearAllTriggerValidate?.()
actionSectionRef.value?.clearAllActionValidate?.()
} }
}) })

View File

@ -1,7 +1,13 @@
<!-- 告警配置组件 --> <!-- 告警配置组件 -->
<template> <template>
<div class="w-full"> <el-form
<el-form-item label="告警配置" required> ref="innerFormRef"
:model="formModel"
:rules="formRules"
label-width="110px"
class="w-full"
>
<el-form-item label="告警配置" prop="alertConfigId" required>
<el-select <el-select
v-model="localValue" v-model="localValue"
placeholder="请选择告警配置" placeholder="请选择告警配置"
@ -16,15 +22,24 @@
:key="config.id" :key="config.id"
:label="config.name" :label="config.name"
:value="config.id" :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-select>
</el-form-item> </el-form-item>
</div> </el-form>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { FormInstance } from 'element-plus'
import { useVModel } from '@vueuse/core' import { useVModel } from '@vueuse/core'
import { AlertConfigApi } from '@/api/iot/alert/config' import { AlertConfigApi } from '@/api/iot/alert/config'
import { buildAlertConfigRules } from '@/views/iot/utils/sceneRule'
/** 告警配置组件 */ /** 告警配置组件 */
defineOptions({ name: 'AlertConfig' }) defineOptions({ name: 'AlertConfig' })
@ -38,9 +53,15 @@ const emit = defineEmits<{
}>() }>()
const localValue = useVModel(props, 'modelValue', emit) const localValue = useVModel(props, 'modelValue', emit)
const innerFormRef = ref<FormInstance>()
const formRules = buildAlertConfigRules()
const loading = ref(false) // const formModel = computed(() => ({
const alertConfigs = ref<any[]>([]) // alertConfigId: localValue.value
}))
const loading = ref(false)
const alertConfigs = ref<any[]>([])
/** /**
* 处理选择变化事件 * 处理选择变化事件
@ -48,19 +69,44 @@ const alertConfigs = ref<any[]>([]) // 告警配置列表
*/ */
const handleChange = (value?: number) => { const handleChange = (value?: number) => {
emit('update:modelValue', value) emit('update:modelValue', value)
nextTick(() => {
innerFormRef.value?.validateField('alertConfigId').catch(() => {})
})
} }
/** 加载告警配置列表 */ /** 加载告警配置列表 */
const loadAlertConfigs = async () => { const loadAlertConfigs = async () => {
loading.value = true loading.value = true
try { try {
alertConfigs.value = (await AlertConfigApi.getSimpleAlertConfigList()) || [] const data = await AlertConfigApi.getAlertConfigPage({
pageNo: 1,
pageSize: 100,
enabled: true
})
alertConfigs.value = data.list || []
} finally { } finally {
loading.value = false 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(() => { onMounted(() => {
loadAlertConfigs() loadAlertConfigs()
}) })

View File

@ -1,10 +1,16 @@
<!-- 单个条件配置组件 --> <!-- 单个条件配置组件 -->
<template> <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-row :gutter="16">
<el-col :span="8"> <el-col :span="8">
<el-form-item label="条件类型" required> <el-form-item label="条件类型" prop="type" required>
<el-select <el-select
:model-value="condition.type" :model-value="condition.type"
@update:model-value="(value) => updateConditionField('type', value)" @update:model-value="(value) => updateConditionField('type', value)"
@ -26,7 +32,7 @@
<!-- 产品设备选择 - 设备相关条件的公共部分 --> <!-- 产品设备选择 - 设备相关条件的公共部分 -->
<el-row v-if="isDeviceCondition" :gutter="16"> <el-row v-if="isDeviceCondition" :gutter="16">
<el-col :span="12"> <el-col :span="12">
<el-form-item label="产品" required> <el-form-item label="产品" prop="productId" required>
<ProductSelector <ProductSelector
:model-value="condition.productId" :model-value="condition.productId"
@update:model-value="(value) => updateConditionField('productId', value)" @update:model-value="(value) => updateConditionField('productId', value)"
@ -35,7 +41,7 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="设备" required> <el-form-item label="设备" prop="deviceId" required>
<DeviceSelector <DeviceSelector
:model-value="condition.deviceId" :model-value="condition.deviceId"
@update:model-value="(value) => updateConditionField('deviceId', value)" @update:model-value="(value) => updateConditionField('deviceId', value)"
@ -51,11 +57,9 @@
v-if="condition.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS" v-if="condition.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS"
class="flex flex-col gap-16px" class="flex flex-col gap-16px"
> >
<!-- 状态和操作符选择 -->
<el-row :gutter="16"> <el-row :gutter="16">
<!-- 操作符选择 -->
<el-col :span="12"> <el-col :span="12">
<el-form-item label="操作符" required> <el-form-item label="操作符" prop="operator" required>
<el-select <el-select
:model-value="condition.operator" :model-value="condition.operator"
@update:model-value="(value) => updateConditionField('operator', value)" @update:model-value="(value) => updateConditionField('operator', value)"
@ -72,9 +76,8 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<!-- 状态选择 -->
<el-col :span="12"> <el-col :span="12">
<el-form-item label="设备状态" required> <el-form-item label="设备状态" prop="param" required>
<el-select <el-select
:model-value="condition.param" :model-value="condition.param"
@update:model-value="(value) => updateConditionField('param', value)" @update:model-value="(value) => updateConditionField('param', value)"
@ -98,11 +101,9 @@
v-else-if="condition.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY" v-else-if="condition.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY"
class="space-y-16px" class="space-y-16px"
> >
<!-- 属性配置 -->
<el-row :gutter="16"> <el-row :gutter="16">
<!-- 属性/事件/服务选择 -->
<el-col :span="6"> <el-col :span="6">
<el-form-item label="监控项" required> <el-form-item label="监控项" prop="identifier" required>
<PropertySelector <PropertySelector
:model-value="condition.identifier" :model-value="condition.identifier"
@update:model-value="(value) => updateConditionField('identifier', value)" @update:model-value="(value) => updateConditionField('identifier', value)"
@ -114,9 +115,8 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<!-- 操作符选择 -->
<el-col :span="6"> <el-col :span="6">
<el-form-item label="操作符" required> <el-form-item label="操作符" prop="operator" required>
<OperatorSelector <OperatorSelector
:model-value="condition.operator" :model-value="condition.operator"
@update:model-value="(value) => updateConditionField('operator', value)" @update:model-value="(value) => updateConditionField('operator', value)"
@ -126,9 +126,8 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<!-- 值输入 -->
<el-col :span="12"> <el-col :span="12">
<el-form-item label="比较值" required> <el-form-item label="比较值" prop="param" required>
<ValueInput <ValueInput
:model-value="condition.param" :model-value="condition.param"
@update:model-value="(value) => updateConditionField('param', value)" @update:model-value="(value) => updateConditionField('param', value)"
@ -146,11 +145,13 @@
v-else-if="condition.type === IotRuleSceneTriggerConditionTypeEnum.CURRENT_TIME" v-else-if="condition.type === IotRuleSceneTriggerConditionTypeEnum.CURRENT_TIME"
:model-value="condition" :model-value="condition"
@update:model-value="updateCondition" @update:model-value="updateCondition"
@field-change="handleCurrentTimeFieldChange"
/> />
</div> </el-form>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { FormInstance } from 'element-plus'
import { useVModel } from '@vueuse/core' import { useVModel } from '@vueuse/core'
import CurrentTimeConditionConfig from './CurrentTimeConditionConfig.vue' import CurrentTimeConditionConfig from './CurrentTimeConditionConfig.vue'
import ProductSelector from '../selectors/ProductSelector.vue' import ProductSelector from '../selectors/ProductSelector.vue'
@ -165,6 +166,7 @@ import {
getConditionTypeOptions, getConditionTypeOptions,
IoTDeviceStatusEnum IoTDeviceStatusEnum
} from '@/views/iot/utils/constants' } from '@/views/iot/utils/constants'
import { buildSubConditionRules } from '@/views/iot/utils/sceneRule'
/** 单个条件配置组件 */ /** 单个条件配置组件 */
defineOptions({ name: 'ConditionConfig' }) defineOptions({ name: 'ConditionConfig' })
@ -203,24 +205,32 @@ const statusOperatorOptions = [
] ]
const condition = useVModel(props, 'modelValue', emit) 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(() => { const isDeviceCondition = computed(() => {
return ( return (
condition.value.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS || condition.value.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS ||
condition.value.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY condition.value.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY
) )
}) // })
const conditionRules = computed(() =>
buildSubConditionRules(condition.value.type, () => condition.value.operator)
)
/** /**
* 更新条件字段 * 更新条件字段
* @param field 字段名 * @param field 字段名
* @param value 字段值 * @param value 字段值
*/ */
const updateConditionField = (field: any, value: any) => { const updateConditionField = (field: keyof TriggerCondition, value: any) => {
;(condition.value as any)[field] = value ;(condition.value as any)[field] = value
emit('update:modelValue', condition.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) emit('update:modelValue', condition.value)
} }
/** 当前时间子组件字段变更后触发校验 */
const handleCurrentTimeFieldChange = (field: string) => {
nextTick(() => {
innerFormRef.value?.validateField(field).catch(() => {})
})
}
/** /**
* 处理条件类型变化事件 * 处理条件类型变化事件
* @param type 条件类型 * @param type 条件类型
*/ */
const handleConditionTypeChange = (type: number) => { const handleConditionTypeChange = (type: number) => {
//
const isCurrentTime = type === IotRuleSceneTriggerConditionTypeEnum.CURRENT_TIME const isCurrentTime = type === IotRuleSceneTriggerConditionTypeEnum.CURRENT_TIME
const isDeviceStatus = type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS const isDeviceStatus = type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS
//
if (isCurrentTime || isDeviceStatus) { if (isCurrentTime || isDeviceStatus) {
condition.value.identifier = undefined condition.value.identifier = undefined
} }
//
if (isCurrentTime) { if (isCurrentTime) {
condition.value.productId = undefined condition.value.productId = undefined
condition.value.deviceId = undefined condition.value.deviceId = undefined
} }
//
condition.value.operator = isCurrentTime condition.value.operator = isCurrentTime
? 'at_time' ? 'at_time'
: IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value : IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value
//
condition.value.param = '' condition.value.param = ''
emit('update:modelValue', condition.value)
nextTick(() => clearValidate())
} }
/** 处理产品变化事件 */ /** 处理产品变化事件 */
const handleProductChange = (_: number) => { const handleProductChange = () => {
//
condition.value.deviceId = undefined condition.value.deviceId = undefined
condition.value.identifier = '' condition.value.identifier = ''
emit('update:modelValue', condition.value)
nextTick(() => {
innerFormRef.value?.clearValidate(['deviceId', 'identifier'])
})
} }
/** 处理设备变化事件 */ /** 处理设备变化事件 */
const handleDeviceChange = (_: number) => { const handleDeviceChange = () => {
//
condition.value.identifier = '' 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 }) => { const handlePropertyChange = (propertyInfo: { type: string; config: any }) => {
propertyType.value = propertyInfo.type propertyType.value = propertyInfo.type
propertyConfig.value = propertyInfo.config propertyConfig.value = propertyInfo.config
//
condition.value.operator = IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value condition.value.operator = IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value
condition.value.param = '' condition.value.param = ''
emit('update:modelValue', condition.value)
} }
/** 处理操作符变化事件 */ /** 处理操作符变化事件 */
const handleOperatorChange = () => { const handleOperatorChange = () => {
//
condition.value.param = '' 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> </script>
<style scoped> <style scoped>

View File

@ -4,7 +4,7 @@
<el-row :gutter="16"> <el-row :gutter="16">
<!-- 时间操作符选择 --> <!-- 时间操作符选择 -->
<el-col :span="8"> <el-col :span="8">
<el-form-item label="时间条件" required> <el-form-item label="时间条件" prop="operator" required>
<el-select <el-select
:model-value="condition.operator" :model-value="condition.operator"
@update:model-value="(value) => updateConditionField('operator', value)" @update:model-value="(value) => updateConditionField('operator', value)"
@ -31,7 +31,7 @@
<!-- 时间值输入 --> <!-- 时间值输入 -->
<el-col :span="8"> <el-col :span="8">
<el-form-item label="时间值" required> <el-form-item label="时间值" prop="param" required>
<el-time-picker <el-time-picker
v-if="needsTimeInput" v-if="needsTimeInput"
:model-value="timeValue" :model-value="timeValue"
@ -59,7 +59,7 @@
<!-- 第二个时间值范围条件 --> <!-- 第二个时间值范围条件 -->
<el-col :span="8" v-if="needsSecondTimeInput"> <el-col :span="8" v-if="needsSecondTimeInput">
<el-form-item label="结束时间" required> <el-form-item label="结束时间" prop="param" required>
<el-time-picker <el-time-picker
v-if="needsTimeInput" v-if="needsTimeInput"
:model-value="timeValue2" :model-value="timeValue2"
@ -99,6 +99,7 @@ const props = defineProps<{
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:modelValue', value: TriggerCondition): void (e: 'update:modelValue', value: TriggerCondition): void
(e: 'field-change', field: string): void
}>() }>()
const condition = useVModel(props, 'modelValue', emit) const condition = useVModel(props, 'modelValue', emit)
@ -187,8 +188,9 @@ const timeValue2 = computed(() => {
* @param field 字段名 * @param field 字段名
* @param value 字段值 * @param value 字段值
*/ */
const updateConditionField = (field: any, value: any) => { const updateConditionField = (field: keyof TriggerCondition, value: any) => {
condition.value[field] = value 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(',') : [] const currentParams = condition.value.param ? condition.value.param.split(',') : []
currentParams[0] = value || '' currentParams[0] = value || ''
//
if (needsSecondTimeInput.value) { if (needsSecondTimeInput.value) {
condition.value.param = currentParams.slice(0, 2).join(',') condition.value.param = currentParams.slice(0, 2).join(',')
} else { } else {
condition.value.param = currentParams[0] 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(',') : [''] const currentParams = condition.value.param ? condition.value.param.split(',') : ['']
currentParams[1] = value || '' currentParams[1] = value || ''
condition.value.param = currentParams.slice(0, 2).join(',') condition.value.param = currentParams.slice(0, 2).join(',')
emit('field-change', 'param')
} }
/** 监听操作符变化,清理不相关的时间值 */ /** 监听操作符变化,清理不相关的时间值 */
@ -222,13 +225,12 @@ watch(
() => condition.value.operator, () => condition.value.operator,
(newOperator) => { (newOperator) => {
if (newOperator === IotRuleSceneTriggerTimeOperatorEnum.TODAY.value) { if (newOperator === IotRuleSceneTriggerTimeOperatorEnum.TODAY.value) {
//
condition.value.param = '' condition.value.param = ''
} else if (!needsSecondTimeInput.value) { } else if (!needsSecondTimeInput.value) {
//
const currentParams = condition.value.param ? condition.value.param.split(',') : [] const currentParams = condition.value.param ? condition.value.param.split(',') : []
condition.value.param = currentParams[0] || '' condition.value.param = currentParams[0] || ''
} }
emit('field-change', 'param')
} }
) )
</script> </script>

View File

@ -1,15 +1,20 @@
<!-- 设备控制配置组件 --> <!-- 设备控制配置组件 -->
<template> <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-row :gutter="16">
<el-col :span="12"> <el-col :span="12">
<el-form-item label="产品" required> <el-form-item label="产品" prop="productId" required>
<ProductSelector v-model="action.productId" @change="handleProductChange" /> <ProductSelector v-model="action.productId" @change="handleProductChange" />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="设备" required> <el-form-item label="设备" prop="deviceId" required>
<DeviceSelector <DeviceSelector
v-model="action.deviceId" v-model="action.deviceId"
:product-id="action.productId" :product-id="action.productId"
@ -21,7 +26,7 @@
<!-- 服务选择 - 服务调用类型时显示 --> <!-- 服务选择 - 服务调用类型时显示 -->
<div v-if="action.productId && isServiceInvokeAction" class="space-y-16px"> <div v-if="action.productId && isServiceInvokeAction" class="space-y-16px">
<el-form-item label="服务" required> <el-form-item label="服务" prop="identifier" required>
<el-select <el-select
v-model="action.identifier" v-model="action.identifier"
placeholder="请选择服务" placeholder="请选择服务"
@ -47,9 +52,8 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
<!-- 服务参数配置 -->
<div v-if="action.identifier" class="space-y-16px"> <div v-if="action.identifier" class="space-y-16px">
<el-form-item label="服务参数" required> <el-form-item label="服务参数" prop="params" required>
<JsonParamsInput <JsonParamsInput
v-model="paramsValue" v-model="paramsValue"
type="service" type="service"
@ -62,8 +66,7 @@
<!-- 控制参数配置 - 属性设置类型时显示 --> <!-- 控制参数配置 - 属性设置类型时显示 -->
<div v-if="action.productId && isPropertySetAction" class="space-y-16px"> <div v-if="action.productId && isPropertySetAction" class="space-y-16px">
<!-- 参数配置 --> <el-form-item label="参数" prop="params" required>
<el-form-item label="参数" required>
<JsonParamsInput <JsonParamsInput
v-model="paramsValue" v-model="paramsValue"
type="property" type="property"
@ -72,10 +75,11 @@
/> />
</el-form-item> </el-form-item>
</div> </div>
</div> </el-form>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { FormInstance } from 'element-plus'
import { useVModel } from '@vueuse/core' import { useVModel } from '@vueuse/core'
import ProductSelector from '../selectors/ProductSelector.vue' import ProductSelector from '../selectors/ProductSelector.vue'
import DeviceSelector from '../selectors/DeviceSelector.vue' import DeviceSelector from '../selectors/DeviceSelector.vue'
@ -88,6 +92,7 @@ import {
IoTDataSpecsDataTypeEnum IoTDataSpecsDataTypeEnum
} from '@/views/iot/utils/constants' } from '@/views/iot/utils/constants'
import { ThingModelApi } from '@/api/iot/thingmodel' import { ThingModelApi } from '@/api/iot/thingmodel'
import { buildDeviceControlRules } from '@/views/iot/utils/sceneRule'
/** 设备控制配置组件 */ /** 设备控制配置组件 */
defineOptions({ name: 'DeviceControlConfig' }) defineOptions({ name: 'DeviceControlConfig' })
@ -101,54 +106,71 @@ const emit = defineEmits<{
}>() }>()
const action = useVModel(props, 'modelValue', emit) const action = useVModel(props, 'modelValue', emit)
const innerFormRef = ref<FormInstance>()
const thingModelProperties = ref<ThingModelProperty[]>([]) // const formRules = computed(() => {
const loadingThingModel = ref(false) // const rules = buildDeviceControlRules(action.value.type)
const selectedService = ref<ThingModelService | null>(null) //
const serviceList = ref<ThingModelService[]>([]) // if (isServiceInvokeAction.value) {
const loadingServices = ref(false) // 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({ const paramsValue = computed({
get: () => { get: () => {
// params JSON
if (action.value.params && typeof action.value.params === 'object') { if (action.value.params && typeof action.value.params === 'object') {
return JSON.stringify(action.value.params, null, 2) return JSON.stringify(action.value.params, null, 2)
} }
// params
return action.value.params || '' return action.value.params || ''
}, },
set: (value: string) => { set: (value: string) => {
// JSON
action.value.params = value.trim() || '' action.value.params = value.trim() || ''
nextTick(() => {
innerFormRef.value?.validateField('params').catch(() => {})
})
} }
}) })
//
const isPropertySetAction = computed(() => { const isPropertySetAction = computed(() => {
return action.value.type === IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET return action.value.type === IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET
}) })
//
const isServiceInvokeAction = computed(() => { const isServiceInvokeAction = computed(() => {
return action.value.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE return action.value.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE
}) })
/** const validateField = (field: string) => {
* 处理产品变化事件 nextTick(() => {
* @param productId 产品 ID innerFormRef.value?.validateField(field).catch(() => {})
*/ })
}
const handleProductChange = (productId?: number) => { const handleProductChange = (productId?: number) => {
//
if (action.value.productId !== productId) { if (action.value.productId !== productId) {
action.value.deviceId = undefined action.value.deviceId = undefined
action.value.identifier = undefined // action.value.identifier = undefined
action.value.params = '' // action.value.params = ''
selectedService.value = null // selectedService.value = null
serviceList.value = [] // serviceList.value = []
} }
//
if (productId) { if (productId) {
if (isPropertySetAction.value) { if (isPropertySetAction.value) {
loadThingModelProperties(productId) loadThingModelProperties(productId)
@ -156,47 +178,37 @@ const handleProductChange = (productId?: number) => {
loadServiceList(productId) loadServiceList(productId)
} }
} }
validateField('productId')
nextTick(() => {
innerFormRef.value?.clearValidate(['deviceId', 'identifier', 'params'])
})
} }
/**
* 处理设备变化事件
* @param deviceId 设备 ID
*/
const handleDeviceChange = (deviceId?: number) => { const handleDeviceChange = (deviceId?: number) => {
//
if (action.value.deviceId !== deviceId) { if (action.value.deviceId !== deviceId) {
action.value.params = '' // action.value.params = ''
} }
validateField('deviceId')
} }
/**
* 处理服务变化事件
* @param serviceIdentifier 服务标识符
*/
const handleServiceChange = (serviceIdentifier?: string) => { const handleServiceChange = (serviceIdentifier?: string) => {
//
const service = serviceList.value.find((s) => s.identifier === serviceIdentifier) || null const service = serviceList.value.find((s) => s.identifier === serviceIdentifier) || null
selectedService.value = service selectedService.value = service
//
action.value.params = '' action.value.params = ''
//
if (service && service.inputParams && service.inputParams.length > 0) { if (service && service.inputParams && service.inputParams.length > 0) {
const defaultParams = {} const defaultParams: Record<string, unknown> = {}
service.inputParams.forEach((param) => { service.inputParams.forEach((param) => {
defaultParams[param.identifier] = getDefaultValueForParam(param) defaultParams[param.identifier] = getDefaultValueForParam(param)
}) })
// JSON
action.value.params = JSON.stringify(defaultParams, null, 2) action.value.params = JSON.stringify(defaultParams, null, 2)
} }
validateField('identifier')
validateField('params')
} }
/**
* 获取物模型TSL数据
* @param productId 产品ID
* @returns 物模型TSL数据
*/
const getThingModelTSL = async (productId: number) => { const getThingModelTSL = async (productId: number) => {
if (!productId) return null if (!productId) return null
@ -208,10 +220,6 @@ const getThingModelTSL = async (productId: number) => {
} }
} }
/**
* 加载物模型属性可写属性
* @param productId 产品ID
*/
const loadThingModelProperties = async (productId: number) => { const loadThingModelProperties = async (productId: number) => {
if (!productId) { if (!productId) {
thingModelProperties.value = [] thingModelProperties.value = []
@ -227,7 +235,6 @@ const loadThingModelProperties = async (productId: number) => {
return return
} }
// accessMode 'w'
thingModelProperties.value = tslData.properties.filter( thingModelProperties.value = tslData.properties.filter(
(property: ThingModelProperty) => (property: ThingModelProperty) =>
property.accessMode && property.accessMode &&
@ -242,10 +249,6 @@ const loadThingModelProperties = async (productId: number) => {
} }
} }
/**
* 加载服务列表
* @param productId 产品ID
*/
const loadServiceList = async (productId: number) => { const loadServiceList = async (productId: number) => {
if (!productId) { if (!productId) {
serviceList.value = [] serviceList.value = []
@ -270,27 +273,14 @@ const loadServiceList = async (productId: number) => {
} }
} }
/**
* 从TSL加载服务信息用于编辑模式回显
* @param productId 产品ID
* @param serviceIdentifier 服务标识符
*/
const loadServiceFromTSL = async (productId: number, serviceIdentifier: string) => { const loadServiceFromTSL = async (productId: number, serviceIdentifier: string) => {
//
await loadServiceList(productId) await loadServiceList(productId)
const service = serviceList.value.find((s) => s.identifier === serviceIdentifier)
//
const service = serviceList.value.find((s: any) => s.identifier === serviceIdentifier)
if (service) { if (service) {
selectedService.value = service selectedService.value = service
} }
} }
/**
* 根据参数类型获取默认值
* @param param 参数对象
* @returns 默认值
*/
const getDefaultValueForParam = (param: any) => { const getDefaultValueForParam = (param: any) => {
switch (param.dataType) { switch (param.dataType) {
case IoTDataSpecsDataTypeEnum.INT: case IoTDataSpecsDataTypeEnum.INT:
@ -303,7 +293,6 @@ const getDefaultValueForParam = (param: any) => {
case IoTDataSpecsDataTypeEnum.TEXT: case IoTDataSpecsDataTypeEnum.TEXT:
return '' return ''
case IoTDataSpecsDataTypeEnum.ENUM: case IoTDataSpecsDataTypeEnum.ENUM:
// 使
if (param.dataSpecs?.dataSpecsList && param.dataSpecs.dataSpecsList.length > 0) { if (param.dataSpecs?.dataSpecsList && param.dataSpecs.dataSpecsList.length > 0) {
return param.dataSpecs.dataSpecsList[0].value 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 () => { const initializeComponent = async () => {
if (isInitialized.value) return if (isInitialized.value) return
const currentAction = action.value const currentAction = action.value
if (!currentAction) return if (!currentAction) return
//
if (currentAction.productId && isPropertySetAction.value) { if (currentAction.productId && isPropertySetAction.value) {
await loadThingModelProperties(currentAction.productId) await loadThingModelProperties(currentAction.productId)
} }
//
if (currentAction.productId && isServiceInvokeAction.value && currentAction.identifier) { if (currentAction.productId && isServiceInvokeAction.value && currentAction.identifier) {
// TSL
await loadServiceFromTSL(currentAction.productId, currentAction.identifier) await loadServiceFromTSL(currentAction.productId, currentAction.identifier)
} }
isInitialized.value = true 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(() => { onMounted(() => {
initializeComponent() initializeComponent()
}) })
/** 监听关键字段的变化,避免深度监听导致的性能问题 */
watch( watch(
() => [action.value.productId, action.value.type, action.value.identifier], () => [action.value.productId, action.value.type, action.value.identifier],
async ([newProductId, , newIdentifier], [oldProductId, , oldIdentifier]) => { async ([newProductId, , newIdentifier], [oldProductId, , oldIdentifier]) => {
//
if (!isInitialized.value) return if (!isInitialized.value) return
//
if (newProductId !== oldProductId) { if (newProductId !== oldProductId) {
if (newProductId && isPropertySetAction.value) { if (newProductId && isPropertySetAction.value) {
await loadThingModelProperties(newProductId as number) await loadThingModelProperties(newProductId as number)
@ -359,18 +356,22 @@ watch(
} }
} }
//
if ( if (
newIdentifier !== oldIdentifier && newIdentifier !== oldIdentifier &&
newProductId && newProductId &&
isServiceInvokeAction.value && isServiceInvokeAction.value &&
newIdentifier newIdentifier
) { ) {
const service = serviceList.value.find((s: any) => s.identifier === newIdentifier) const service = serviceList.value.find((s) => s.identifier === newIdentifier)
if (service) { if (service) {
selectedService.value = service selectedService.value = service
} }
} }
} }
) )
watch(
() => action.value.type,
() => nextTick(() => clearValidate())
)
</script> </script>

View File

@ -26,6 +26,7 @@
<!-- 主条件内容配置 --> <!-- 主条件内容配置 -->
<MainConditionInnerConfig <MainConditionInnerConfig
ref="mainConditionRef"
:model-value="trigger" :model-value="trigger"
@update:model-value="updateCondition" @update:model-value="updateCondition"
:trigger-type="trigger.type" :trigger-type="trigger.type"
@ -118,6 +119,7 @@
</div> </div>
<SubConditionGroupConfig <SubConditionGroupConfig
:ref="(el) => setSubGroupRef(el, subGroupIndex)"
:model-value="subGroup" :model-value="subGroup"
@update:model-value="(value) => updateSubGroup(subGroupIndex, value)" @update:model-value="(value) => updateSubGroup(subGroupIndex, value)"
:trigger-type="trigger.type" :trigger-type="trigger.type"
@ -184,6 +186,22 @@ const emit = defineEmits<{
}>() }>()
const trigger = useVModel(props, 'modelValue', emit) 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 maxSubGroups = 3 // 3
const maxConditionsPerGroup = 3 // 3 const maxConditionsPerGroup = 3 // 3
@ -248,4 +266,35 @@ const updateSubGroup = (index: number, subGroup: any) => {
const removeConditionGroup = () => { const removeConditionGroup = () => {
trigger.value.conditionGroups = undefined 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> </script>

View File

@ -1,5 +1,11 @@
<template> <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-form-item label="触发事件类型" required>
<el-select <el-select
@ -22,7 +28,7 @@
<!-- 产品设备选择 --> <!-- 产品设备选择 -->
<el-row :gutter="16"> <el-row :gutter="16">
<el-col :span="12"> <el-col :span="12">
<el-form-item label="产品" required> <el-form-item label="产品" prop="productId" required>
<ProductSelector <ProductSelector
:model-value="condition.productId" :model-value="condition.productId"
@update:model-value="(value) => updateConditionField('productId', value)" @update:model-value="(value) => updateConditionField('productId', value)"
@ -31,7 +37,7 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="设备" required> <el-form-item label="设备" prop="deviceId" required>
<DeviceSelector <DeviceSelector
:model-value="condition.deviceId" :model-value="condition.deviceId"
@update:model-value="(value) => updateConditionField('deviceId', value)" @update:model-value="(value) => updateConditionField('deviceId', value)"
@ -46,7 +52,7 @@
<el-row :gutter="16"> <el-row :gutter="16">
<!-- 属性/事件/服务选择 --> <!-- 属性/事件/服务选择 -->
<el-col :span="6"> <el-col :span="6">
<el-form-item label="监控项" required> <el-form-item label="监控项" prop="identifier" required>
<PropertySelector <PropertySelector
:model-value="condition.identifier" :model-value="condition.identifier"
@update:model-value="(value) => updateConditionField('identifier', value)" @update:model-value="(value) => updateConditionField('identifier', value)"
@ -60,7 +66,7 @@
<!-- 操作符选择 - 服务调用和事件上报不需要操作符 --> <!-- 操作符选择 - 服务调用和事件上报不需要操作符 -->
<el-col v-if="needsOperatorSelector" :span="6"> <el-col v-if="needsOperatorSelector" :span="6">
<el-form-item label="操作符" required> <el-form-item label="操作符" prop="operator" required>
<OperatorSelector <OperatorSelector
:model-value="condition.operator" :model-value="condition.operator"
@update:model-value="(value) => updateConditionField('operator', value)" @update:model-value="(value) => updateConditionField('operator', value)"
@ -71,7 +77,7 @@
<!-- 值输入 --> <!-- 值输入 -->
<el-col :span="isWideValueColumn ? 18 : 12"> <el-col :span="isWideValueColumn ? 18 : 12">
<el-form-item :label="valueInputLabel" required> <el-form-item :label="valueInputLabel" prop="value" :required="needsValueRequired">
<!-- 服务调用参数配置 --> <!-- 服务调用参数配置 -->
<JsonParamsInput <JsonParamsInput
v-if="triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE" v-if="triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE"
@ -113,7 +119,7 @@
<!-- 设备状态触发器使用简化的配置 --> <!-- 设备状态触发器使用简化的配置 -->
<el-row :gutter="16"> <el-row :gutter="16">
<el-col :span="12"> <el-col :span="12">
<el-form-item label="产品" required> <el-form-item label="产品" prop="productId" required>
<ProductSelector <ProductSelector
:model-value="condition.productId" :model-value="condition.productId"
@update:model-value="(value) => updateConditionField('productId', value)" @update:model-value="(value) => updateConditionField('productId', value)"
@ -122,7 +128,7 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="设备" required> <el-form-item label="设备" prop="deviceId" required>
<DeviceSelector <DeviceSelector
:model-value="condition.deviceId" :model-value="condition.deviceId"
@update:model-value="(value) => updateConditionField('deviceId', value)" @update:model-value="(value) => updateConditionField('deviceId', value)"
@ -134,7 +140,7 @@
</el-row> </el-row>
<el-row :gutter="16"> <el-row :gutter="16">
<el-col :span="6"> <el-col :span="6">
<el-form-item label="操作符" required> <el-form-item label="操作符" prop="operator" required>
<el-select <el-select
:model-value="condition.operator" :model-value="condition.operator"
@update:model-value="(value) => updateConditionField('operator', value)" @update:model-value="(value) => updateConditionField('operator', value)"
@ -149,11 +155,11 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="6"> <el-col :span="6">
<el-form-item label="参数" required> <el-form-item label="参数" prop="value" required>
<el-select <el-select
:model-value="condition.value" :model-value="condition.value"
@update:model-value="(value) => updateConditionField('value', value)" @update:model-value="(value) => updateConditionField('value', value)"
placeholder="请选择操作符" placeholder="请选择设备状态"
class="w-full" class="w-full"
> >
<el-option <el-option
@ -177,10 +183,11 @@
此触发类型暂不需要配置额外条件 此触发类型暂不需要配置额外条件
</p> </p>
</div> </div>
</div> </el-form>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { FormInstance } from 'element-plus'
import ProductSelector from '../selectors/ProductSelector.vue' import ProductSelector from '../selectors/ProductSelector.vue'
import DeviceSelector from '../selectors/DeviceSelector.vue' import DeviceSelector from '../selectors/DeviceSelector.vue'
import PropertySelector from '../selectors/PropertySelector.vue' import PropertySelector from '../selectors/PropertySelector.vue'
@ -196,6 +203,7 @@ import {
IotRuleSceneTriggerConditionParameterOperatorEnum, IotRuleSceneTriggerConditionParameterOperatorEnum,
IoTDeviceStatusEnum IoTDeviceStatusEnum
} from '@/views/iot/utils/constants' } from '@/views/iot/utils/constants'
import { buildMainConditionRules } from '@/views/iot/utils/sceneRule'
import { useVModel } from '@vueuse/core' import { useVModel } from '@vueuse/core'
/** 主条件内部配置组件 */ /** 主条件内部配置组件 */
@ -224,9 +232,12 @@ const deviceStatusChangeOptions = [
] ]
const condition = useVModel(props, 'modelValue', emit) const condition = useVModel(props, 'modelValue', emit)
const innerFormRef = ref<FormInstance>()
const propertyType = ref('') // const propertyType = ref('') //
const propertyConfig = ref<any>(null) // const propertyConfig = ref<any>(null) //
const conditionRules = computed(() => buildMainConditionRules(props.triggerType))
// //
const isDevicePropertyTrigger = computed(() => { const isDevicePropertyTrigger = computed(() => {
return ( return (
@ -250,6 +261,11 @@ const needsOperatorSelector = computed(() => {
return !noOperatorTriggerTypes.includes(props.triggerType) return !noOperatorTriggerTypes.includes(props.triggerType)
}) })
// /
const needsValueRequired = computed(() => {
return props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST
})
// //
const isWideValueColumn = computed(() => { const isWideValueColumn = computed(() => {
const wideColumnTriggerTypes = [ const wideColumnTriggerTypes = [
@ -282,13 +298,26 @@ const serviceConfig = computed(() => {
return undefined 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 field 字段名
* @param value 字段值 * @param value 字段值
*/ */
const updateConditionField = (field: any, value: any) => { const updateConditionField = (field: keyof Trigger, value: any) => {
condition.value[field] = value condition.value[field] = value
nextTick(() => {
innerFormRef.value?.validateField(field as string).catch(() => {})
})
} }
/** /**
@ -301,15 +330,19 @@ const handleTriggerTypeChange = (type: number) => {
/** 处理产品变化事件 */ /** 处理产品变化事件 */
const handleProductChange = () => { const handleProductChange = () => {
//
condition.value.deviceId = undefined condition.value.deviceId = undefined
condition.value.identifier = '' condition.value.identifier = ''
nextTick(() => {
innerFormRef.value?.clearValidate(['deviceId', 'identifier'])
})
} }
/** 处理设备变化事件 */ /** 处理设备变化事件 */
const handleDeviceChange = () => { const handleDeviceChange = () => {
//
condition.value.identifier = '' condition.value.identifier = ''
nextTick(() => {
innerFormRef.value?.clearValidate('identifier')
})
} }
/** /**
@ -321,7 +354,6 @@ const handlePropertyChange = (propertyInfo: any) => {
propertyType.value = propertyInfo.type propertyType.value = propertyInfo.type
propertyConfig.value = propertyInfo.config propertyConfig.value = propertyInfo.config
// '='
if ( if (
props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST || props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST ||
props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE 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> </script>

View File

@ -53,6 +53,7 @@
<div class="p-12px"> <div class="p-12px">
<ConditionConfig <ConditionConfig
:ref="(el) => setConditionRef(el, conditionIndex)"
:model-value="condition" :model-value="condition"
@update:model-value="(value) => updateCondition(conditionIndex, value)" @update:model-value="(value) => updateCondition(conditionIndex, value)"
:trigger-type="triggerType" :trigger-type="triggerType"
@ -105,6 +106,44 @@ const subGroup = useVModel(props, 'modelValue', emit)
const maxConditions = computed(() => props.maxConditions || 3) // 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 () => { const addCondition = async () => {
// subGroup.value // subGroup.value

View File

@ -67,6 +67,7 @@
</div> </div>
<SubConditionGroupConfig <SubConditionGroupConfig
:ref="(el) => setSubGroupRef(el, groupIndex)"
:model-value="group" :model-value="group"
@update:model-value="(value) => updateConditionGroup(groupIndex, value)" @update:model-value="(value) => updateConditionGroup(groupIndex, value)"
:trigger-type="IotRuleSceneTriggerTypeEnum.TIMER" :trigger-type="IotRuleSceneTriggerTypeEnum.TIMER"
@ -131,6 +132,44 @@ const conditionGroups = useVModel(props, 'modelValue', emit)
const maxGroups = 3 // 3 const maxGroups = 3 // 3
const maxConditionsPerGroup = 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 () => { const addConditionGroup = async () => {
if (!conditionGroups.value) { if (!conditionGroups.value) {

View File

@ -141,6 +141,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useVModel } from '@vueuse/core' import { useVModel } from '@vueuse/core'
import { InfoFilled } from '@element-plus/icons-vue' import { InfoFilled } from '@element-plus/icons-vue'
import { isEmptyVal } from '@/utils/is'
import { import {
IoTDataSpecsDataTypeEnum, IoTDataSpecsDataTypeEnum,
JSON_PARAMS_INPUT_CONSTANTS, JSON_PARAMS_INPUT_CONSTANTS,
@ -329,7 +330,8 @@ const handleParamsChange = () => {
// //
for (const param of paramsList.value) { 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) jsonError.value = JSON_PARAMS_INPUT_CONSTANTS.PARAM_REQUIRED_ERROR(param.name)
return return
} }

View File

@ -75,7 +75,7 @@
<el-select <el-select
:model-value="action.type" :model-value="action.type"
@update:model-value="(value) => updateActionType(index, value)" @update:model-value="(value) => updateActionType(index, value)"
@change="(value) => onActionTypeChange(action, value)" @change="(value) => onActionTypeChange(action, value, index)"
placeholder="请选择执行类型" placeholder="请选择执行类型"
class="w-full" class="w-full"
> >
@ -92,6 +92,7 @@
<!-- 设备控制配置 --> <!-- 设备控制配置 -->
<DeviceControlConfig <DeviceControlConfig
v-if="isDeviceAction(action.type)" v-if="isDeviceAction(action.type)"
:ref="(el) => setDeviceControlRef(el, index)"
:model-value="action" :model-value="action"
@update:model-value="(value) => updateAction(index, value)" @update:model-value="(value) => updateAction(index, value)"
/> />
@ -99,6 +100,7 @@
<!-- 告警配置 - 只有恢复告警时才显示 --> <!-- 告警配置 - 只有恢复告警时才显示 -->
<AlertConfig <AlertConfig
v-if="action.type === IotRuleSceneActionTypeEnum.ALERT_RECOVER" v-if="action.type === IotRuleSceneActionTypeEnum.ALERT_RECOVER"
:ref="(el) => setAlertConfigRef(el, index)"
:model-value="action.alertConfigId" :model-value="action.alertConfigId"
@update:model-value="(value) => updateActionAlertConfig(index, value)" @update:model-value="(value) => updateActionAlertConfig(index, value)"
/> />
@ -156,6 +158,66 @@ const emit = defineEmits<{
const actions = useVModel(props, 'actions', emit) 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 属性) */ /** 获取执行器标签类型(用于 el-tag 的 type 属性) */
const getActionTypeTag = (type: number): 'primary' | 'success' | 'info' | 'warning' | 'danger' => { const getActionTypeTag = (type: number): 'primary' | 'success' | 'info' | 'warning' | 'danger' => {
const actionTypeTags = { const actionTypeTags = {
@ -222,9 +284,8 @@ const removeAction = (index: number) => {
* @param type 执行器类型 * @param type 执行器类型
*/ */
const updateActionType = (index: number, type: number) => { const updateActionType = (index: number, type: number) => {
const action = actions.value[index] actions.value[index].type = type
onActionTypeChange(action, type) // action.type onActionTypeChange(actions.value[index], type, index)
action.type = type
} }
/** /**
@ -250,7 +311,7 @@ const updateActionAlertConfig = (index: number, alertConfigId?: number) => {
* @param action 执行器对象 * @param action 执行器对象
* @param type 执行器类型 * @param type 执行器类型
*/ */
const onActionTypeChange = (action: Action, type: number) => { const onActionTypeChange = (action: Action, type: number, index: number) => {
// //
if (isDeviceAction(type)) { if (isDeviceAction(type)) {
// //
@ -269,5 +330,10 @@ const onActionTypeChange = (action: Action, type: number) => {
action.params = undefined action.params = undefined
action.alertConfigId = undefined action.alertConfigId = undefined
} }
nextTick(() => {
deviceControlRefs.value[index]?.clearValidate?.()
alertConfigRefs.value[index]?.clearValidate?.()
})
} }
</script> </script>

View File

@ -59,6 +59,7 @@
<!-- 设备触发配置 --> <!-- 设备触发配置 -->
<DeviceTriggerConfig <DeviceTriggerConfig
v-if="isDeviceTrigger(triggerItem.type)" v-if="isDeviceTrigger(triggerItem.type)"
:ref="(el) => setDeviceTriggerRef(el, index)"
:model-value="triggerItem" :model-value="triggerItem"
:index="index" :index="index"
@update:model-value="(value) => updateTriggerDeviceConfig(index, value)" @update:model-value="(value) => updateTriggerDeviceConfig(index, value)"
@ -93,6 +94,7 @@
<!-- 附加条件组配置 --> <!-- 附加条件组配置 -->
<TimerConditionGroupConfig <TimerConditionGroupConfig
:ref="(el) => setTimerConditionRef(el, index)"
:model-value="triggerItem.conditionGroups" :model-value="triggerItem.conditionGroups"
@update:model-value="(value) => updateTriggerConditionGroups(index, value)" @update:model-value="(value) => updateTriggerConditionGroups(index, value)"
/> />
@ -127,6 +129,7 @@ import type { Trigger, TriggerCondition } from '@/api/iot/rule/scene'
import { import {
getTriggerTypeLabel, getTriggerTypeLabel,
IotRuleSceneTriggerTypeEnum, IotRuleSceneTriggerTypeEnum,
IotRuleSceneTriggerConditionParameterOperatorEnum,
isDeviceTrigger isDeviceTrigger
} from '@/views/iot/utils/constants' } from '@/views/iot/utils/constants'
@ -143,6 +146,72 @@ const emit = defineEmits<{
const triggers = useVModel(props, 'triggers', emit) 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 属性) */ /** 获取触发器标签类型(用于 el-tag 的 type 属性) */
const getTriggerTagType = (type: number): 'primary' | 'success' | 'info' | 'warning' | 'danger' => { const getTriggerTagType = (type: number): 'primary' | 'success' | 'info' | 'warning' | 'danger' => {
if (type === IotRuleSceneTriggerTypeEnum.TIMER) { if (type === IotRuleSceneTriggerTypeEnum.TIMER) {
@ -158,7 +227,7 @@ const addTrigger = () => {
productId: undefined, productId: undefined,
deviceId: undefined, deviceId: undefined,
identifier: undefined, identifier: undefined,
operator: undefined, operator: IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value,
value: undefined, value: undefined,
cronExpression: undefined, cronExpression: undefined,
conditionGroups: [] // conditionGroups: [] //
@ -218,7 +287,7 @@ const updateTriggerConditionGroups = (index: number, conditionGroups: TriggerCon
* @param index 触发器索引 * @param index 触发器索引
* @param _ 触发器类型未使用 * @param _ 触发器类型未使用
*/ */
const onTriggerTypeChange = (index: number, _: number) => { const onTriggerTypeChange = (index: number, type: number) => {
const triggerItem = triggers.value[index] const triggerItem = triggers.value[index]
triggerItem.productId = undefined triggerItem.productId = undefined
triggerItem.deviceId = undefined triggerItem.deviceId = undefined
@ -227,6 +296,10 @@ const onTriggerTypeChange = (index: number, _: number) => {
triggerItem.value = undefined triggerItem.value = undefined
triggerItem.cronExpression = undefined triggerItem.cronExpression = undefined
triggerItem.conditionGroups = [] triggerItem.conditionGroups = []
if (type === IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE) {
triggerItem.operator = IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value
}
nextTick(() => deviceTriggerRefs.value[index]?.clearValidate?.())
} }
/** 初始化:确保至少有一个触发器 */ /** 初始化:确保至少有一个触发器 */

View File

@ -25,6 +25,13 @@ export const IoTThingModelTypeEnum = {
EVENT: 3 // 事件 EVENT: 3 // 事件
} as const } as const
/** IoT 告警接收方式枚举,与后端 IotAlertReceiveTypeEnum 保持一致 */
export const IotAlertReceiveTypeEnum = {
SMS: 1, // 短信
MAIL: 2, // 邮箱
NOTIFY: 3 // 站内信
} as const
/** IoT 设备消息的方法枚举 */ /** IoT 设备消息的方法枚举 */
export const IotDeviceMessageMethodEnum = { export const IotDeviceMessageMethodEnum = {
// ========== 设备状态 ========== // ========== 设备状态 ==========

View File

@ -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
}

View File

@ -10,6 +10,9 @@
<el-form-item label="手机号" prop="mobile"> <el-form-item label="手机号" prop="mobile">
<el-input v-model="formData.mobile" placeholder="请输入手机号" /> <el-input v-model="formData.mobile" placeholder="请输入手机号" />
</el-form-item> </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-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status"> <el-radio-group v-model="formData.status">
<el-radio <el-radio
@ -91,6 +94,7 @@ const formType = ref('') // 表单的类型create - 新增update - 修改
const formData = ref({ const formData = ref({
id: undefined, id: undefined,
mobile: undefined, mobile: undefined,
email: undefined,
password: undefined, password: undefined,
status: undefined, status: undefined,
nickname: undefined, nickname: undefined,
@ -105,6 +109,7 @@ const formData = ref({
}) })
const formRules = reactive({ const formRules = reactive({
mobile: [{ required: true, message: '手机号不能为空', trigger: 'blur' }], mobile: [{ required: true, message: '手机号不能为空', trigger: 'blur' }],
email: [{ type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change'] }],
status: [{ required: true, message: '状态不能为空', trigger: 'blur' }] status: [{ required: true, message: '状态不能为空', trigger: 'blur' }]
}) })
const formRef = ref() // Ref const formRef = ref() // Ref
@ -162,6 +167,7 @@ const resetForm = () => {
formData.value = { formData.value = {
id: undefined, id: undefined,
mobile: undefined, mobile: undefined,
email: undefined,
password: undefined, password: undefined,
status: undefined, status: undefined,
nickname: undefined, nickname: undefined,

View File

@ -27,6 +27,12 @@
</template> </template>
{{ user.mobile }} {{ user.mobile }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item>
<template #label>
<descriptions-item-label icon="ep:message" label="邮箱" />
</template>
{{ user.email || '空' }}
</el-descriptions-item>
<el-descriptions-item> <el-descriptions-item>
<template #label> <template #label>
<descriptions-item-label icon="fa:mars-double" label="性别" /> <descriptions-item-label icon="fa:mars-double" label="性别" />
@ -87,6 +93,12 @@
</template> </template>
{{ user.mobile }} {{ user.mobile }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item>
<template #label>
<descriptions-item-label icon="ep:message" label="邮箱" />
</template>
{{ user.email || '空' }}
</el-descriptions-item>
<el-descriptions-item> <el-descriptions-item>
<template #label> <template #label>
<descriptions-item-label icon="fa:mars-double" label="性别" /> <descriptions-item-label icon="fa:mars-double" label="性别" />

View File

@ -28,6 +28,15 @@
@keyup.enter="handleQuery" @keyup.enter="handleQuery"
/> />
</el-form-item> </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-form-item label="注册时间" prop="createTime">
<el-date-picker <el-date-picker
v-model="queryParams.createTime" v-model="queryParams.createTime"
@ -90,6 +99,7 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column align="center" label="手机号" prop="mobile" width="120px" /> <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="nickname" width="80px" />
<el-table-column align="center" label="等级" prop="levelName" width="100px" /> <el-table-column align="center" label="等级" prop="levelName" width="100px" />
<el-table-column align="center" label="分组" prop="groupName" width="100px" /> <el-table-column align="center" label="分组" prop="groupName" width="100px" />
@ -227,6 +237,7 @@ const queryParams = reactive({
pageSize: 10, pageSize: 10,
nickname: null, nickname: null,
mobile: null, mobile: null,
email: null,
loginDate: [], loginDate: [],
createTime: [], createTime: [],
tagIds: [], tagIds: [],

View File

@ -44,7 +44,7 @@
v-model="scope.row.handlerRoleId" v-model="scope.row.handlerRoleId"
placeholder="请选择角色" placeholder="请选择角色"
/> />
<RoleSelect v-else :model-value="scope.row.handlerRoleId" disabled /> <span v-else>{{ scope.row.handlerRoleName || '-' }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="处置人" align="center" width="180"> <el-table-column label="处置人" align="center" width="180">

View File

@ -19,7 +19,9 @@
:disabled="isHeaderReadonly" :disabled="isHeaderReadonly"
> >
<template #append> <template #append>
<el-button @click="generateCode"> </el-button> <el-button :disabled="isHeaderReadonly" @click="generateCode">
生成
</el-button>
</template> </template>
</el-input> </el-input>
</el-form-item> </el-form-item>

View File

@ -40,7 +40,7 @@
:disabled="isHeaderReadonly" :disabled="isHeaderReadonly"
> >
<el-option <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" :key="dict.value"
:label="dict.label" :label="dict.label"
:value="dict.value" :value="dict.value"
@ -120,7 +120,7 @@
</template> </template>
<script setup lang="ts"> <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 { WmMiscIssueApi, WmMiscIssueVO } from '@/api/mes/wm/miscissue'
import { AutoCodeRecordApi } from '@/api/mes/md/autocode/record' import { AutoCodeRecordApi } from '@/api/mes/md/autocode/record'
import MiscIssueLineList from './MiscIssueLineList.vue' import MiscIssueLineList from './MiscIssueLineList.vue'

View File

@ -35,7 +35,7 @@
class="!w-240px" class="!w-240px"
> >
<el-option <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" :key="dict.value"
:label="dict.label" :label="dict.label"
:value="dict.value" :value="dict.value"
@ -195,7 +195,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { dateFormatter2 } from '@/utils/formatTime' 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 download from '@/utils/download'
import { WmMiscIssueApi, WmMiscIssueVO } from '@/api/mes/wm/miscissue' import { WmMiscIssueApi, WmMiscIssueVO } from '@/api/mes/wm/miscissue'
import MiscIssueForm from './MiscIssueForm.vue' import MiscIssueForm from './MiscIssueForm.vue'

View File

@ -118,13 +118,12 @@
</template> </template>
<template v-else-if="formData.type === MesWmStockTakingParamTypeEnum.QUALITY_STATUS"> <template v-else-if="formData.type === MesWmStockTakingParamTypeEnum.QUALITY_STATUS">
<el-select <el-select
v-model="formData.valueCode" v-model="qualityStatusValue"
placeholder="请选择质量状态" placeholder="请选择质量状态"
class="!w-full" class="!w-full"
@change="handleQualityStatusChange"
> >
<el-option <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" :key="dict.value"
:label="dict.label" :label="dict.label"
:value="dict.value" :value="dict.value"
@ -148,7 +147,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict' import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { import {
StockTakingPlanParamApi, StockTakingPlanParamApi,
type StockTakingPlanParamVO type StockTakingPlanParamVO
@ -219,6 +218,10 @@ const formData = ref<StockTakingPlanParamVO>({
valueName: '', valueName: '',
remark: '' remark: ''
}) })
const qualityStatusValue = computed({
get: () => (formData.value.valueCode ? Number(formData.value.valueCode) : undefined),
set: (val?: number) => handleQualityStatusChange(val)
})
const formRules = reactive({ const formRules = reactive({
type: [{ required: true, message: '请选择条件类型', trigger: 'change' }], type: [{ required: true, message: '请选择条件类型', trigger: 'change' }],
valueId: [ valueId: [
@ -331,11 +334,11 @@ const handleBatchChange = (batch?: any) => {
} }
/** 质量状态选择器变化 */ /** 质量状态选择器变化 */
const handleQualityStatusChange = (val: string) => { const handleQualityStatusChange = (val?: number) => {
const dictOptions = getStrDictOptions(DICT_TYPE.MES_WM_QUALITY_STATUS) const dictOptions = getIntDictOptions(DICT_TYPE.MES_WM_QUALITY_STATUS)
const selected = dictOptions.find((d) => d.value === val) const selected = dictOptions.find((d) => d.value === val)
formData.value.valueId = undefined // ID formData.value.valueId = undefined // ID
formData.value.valueCode = val formData.value.valueCode = val == null ? '' : String(val)
formData.value.valueName = selected?.label || '' formData.value.valueName = selected?.label || ''
} }

View File

@ -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>

View File

@ -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>

View File

@ -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>