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 // 关联的场景联动规则编号数组
receiveUserIds: string // 接收的用户编号数组
receiveTypes: string // 接收的类型数组
smsTemplateCode?: string // 短信模板编号
mailTemplateCode?: string // 邮件模板编号
notifyTemplateCode?: string // 站内信模板编号
}
// IoT 告警配置 API

View File

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

View File

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

View File

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

View File

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

View File

@ -19,6 +19,16 @@ export interface MailSendReqVO {
templateParams: Map<String, Object>
}
export interface MailTemplateSimpleVO {
id: number
name: string
code: string
}
// 查询邮件模版精简列表
export const getSimpleMailTemplateList = async () => {
return await request.get({ url: '/system/mail-template/simple-list' })
}
// 查询邮件模版列表
export const getMailTemplatePage = async (params: PageParam) => {
return await request.get({ url: '/system/mail-template/page', params })

View File

@ -18,6 +18,17 @@ export interface NotifySendReqVO {
templateParams: Map<String, Object>
}
export interface NotifyTemplateSimpleVO {
id: number
name: string
code: string
}
// 查询站内信模板精简列表
export const getSimpleNotifyTemplateList = async () => {
return await request.get({ url: '/system/notify-template/simple-list' })
}
// 查询站内信模板列表
export const getNotifyTemplatePage = async (params: PageParam) => {
return await request.get({ url: '/system/notify-template/page', params })
@ -45,7 +56,10 @@ export const deleteNotifyTemplate = async (id: number) => {
// 批量删除站内信模板
export const deleteNotifyTemplateList = async (ids: number[]) => {
return await request.delete({ url: '/system/notify-template/delete-list', params: { ids: ids.join(',') } })
return await request.delete({
url: '/system/notify-template/delete-list',
params: { ids: ids.join(',') }
})
}
// 发送站内信

View File

@ -21,6 +21,16 @@ export interface SendSmsReqVO {
templateParams: Map<String, Object>
}
export interface SmsTemplateSimpleVO {
id: number
name: string
code: string
}
// 查询短信模板精简列表
export const getSimpleSmsTemplateList = () => {
return request.get({ url: '/system/sms-template/simple-list' })
}
// 查询短信模板列表
export const getSmsTemplatePage = (params: PageParam) => {
return request.get({ url: '/system/sms-template/page', params })

View File

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

View File

@ -1,10 +1,53 @@
<script setup lang="ts">
import * as ProcessInstanceApi from '@/api/bpm/processInstance'
import * as AreaApi from '@/api/system/area'
import * as DeptApi from '@/api/system/dept'
import * as UserApi from '@/api/system/user'
import { useUserStore } from '@/store/modules/user'
import { formatDate } from '@/utils/formatTime'
import { DICT_TYPE, getDictLabel } from '@/utils/dict'
import {
DICT_TYPE,
getBoolDictOptions,
getDictLabel,
getDictOptions,
getIntDictOptions,
getStrDictOptions
} from '@/utils/dict'
import { decodeFields } from '@/utils/formCreate'
interface FormFieldItem {
html: string
id: string
name: string
}
interface FormFieldOption {
label?: string
value?: unknown
}
type FormFieldRule = Record<string, unknown> & {
field?: string
options?: FormFieldOption[]
props?: Record<string, unknown>
title?: string
type?: string
}
type PrintableRecord = Record<string, unknown>
interface AreaNode {
children?: AreaNode[]
id?: number
name: string
}
interface PrintLookupMaps {
areaMap: Map<string, string>
deptMap: Map<string, string>
userMap: Map<string, string>
}
const userStore = useUserStore()
const visible = ref(false)
@ -13,15 +56,16 @@ const loading = ref(false)
const printData = ref()
const userName = computed(() => userStore.user.nickname ?? '')
const printTime = ref(formatDate(new Date(), 'YYYY-MM-DD HH:mm'))
const formFields = ref()
const printDataMap = ref({})
const formFields = ref<FormFieldItem[]>([])
const printDataMap = ref<Record<string, string>>({})
const open = async (id: string) => {
loading.value = true
try {
printData.value = await ProcessInstanceApi.getProcessInstancePrintData(id)
printTime.value = formatDate(new Date(), 'YYYY-MM-DD HH:mm')
initPrintDataMap()
parseFormFields()
await parseFormFields()
} finally {
loading.value = false
}
@ -29,82 +73,347 @@ const open = async (id: string) => {
}
defineExpose({ open })
const parseFormFields = () => {
const parseFormFields = async () => {
if (!printData.value) return
const formFieldsObj = decodeFields(
printData.value.processInstance.processDefinition?.formFields || []
)
const processVariables = printData.value.processInstance.formVariables
let res: any = []
) as FormFieldRule[]
const processVariables = printData.value.processInstance.formVariables ?? {}
const lookupMaps = await loadPrintLookupMaps(formFieldsObj)
const res: FormFieldItem[] = []
for (const item of formFieldsObj) {
const id = item['field']
const name = item['title']
const variable = processVariables[item['field']]
let html = variable
switch (item['type']) {
case 'UploadImg': {
let imgEl = document.createElement('img')
imgEl.setAttribute('src', variable)
imgEl.setAttribute('style', 'max-width: 600px;')
html = imgEl.outerHTML
break
}
case 'radio':
case 'checkbox':
case 'select': {
const options = item['options'] || []
const temp: any = []
if (Array.isArray(variable)) {
const labels = options.filter((o) => variable.includes(o.value)).map((o) => o.label)
temp.push(...labels)
} else {
const opt = options.find((o) => o.value === variable)
temp.push(opt.label)
}
html = temp.join(',')
}
// TODO
}
printDataMap.value[item['field']] = html
const fieldKey = String(item.field ?? '')
const id = fieldKey
const name = String(item.title ?? fieldKey)
const variable = processVariables[fieldKey]
const html = formatPrintField(item, variable, lookupMaps)
printDataMap.value[fieldKey] = html
res.push({ id, name, html })
}
formFields.value = res
}
const getRuleProp = (rule: FormFieldRule, key: string) => {
return rule?.[key] ?? rule?.props?.[key]
}
const isPrintableRecord = (value: unknown): value is PrintableRecord => {
return typeof value === 'object' && value !== null
}
const getRecordValue = (record: PrintableRecord, key: string) => {
return record[key]
}
const isNotEmptyString = (value: string) => value.length > 0
const isEmptyValue = (value: unknown) => value === undefined || value === null || value === ''
const toValueArray = (value: unknown) => {
if (Array.isArray(value)) {
return value
}
if (isEmptyValue(value)) {
return []
}
return [value]
}
const escapeHtml = (value: unknown) => {
return String(value)
.replace(/&/g, '&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 = () => {
printDataMap.value['startUser'] = printData.value.processInstance.startUser.nickname
printDataMap.value['startUserDept'] = printData.value.processInstance.startUser.deptName
if (!printData.value) return
printDataMap.value['startUser'] = printData.value.processInstance.startUser?.nickname || ''
printDataMap.value['startUserDept'] = printData.value.processInstance.startUser?.deptName || ''
printDataMap.value['processName'] = printData.value.processInstance.name
printDataMap.value['processNum'] = printData.value.processInstance.id
printDataMap.value['processNum'] = String(printData.value.processInstance.id ?? '')
printDataMap.value['startTime'] = formatDate(printData.value.processInstance.startTime)
printDataMap.value['endTime'] = formatDate(printData.value.processInstance.endTime)
printDataMap.value['processStatus'] = getDictLabel(
printDataMap.value['processStatus'] = String(getDictLabel(
DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS,
printData.value.processInstance.status
)
) ?? '')
printDataMap.value['printUser'] = userName.value
printDataMap.value['printTime'] = printTime.value
}
const getPrintTemplateHTML = () => {
if (!printData.value?.printTemplateHtml) return ''
const parser = new DOMParser()
let doc = parser.parseFromString(printData.value.printTemplateHtml, 'text/html')
const doc = parser.parseFromString(printData.value.printTemplateHtml, 'text/html')
// table border
let tables = doc.querySelectorAll('table')
const tables = doc.querySelectorAll('table')
tables.forEach((item) => {
item.setAttribute('border', '1')
item.setAttribute('style', (item.getAttribute('style') || '') + 'border-collapse:collapse;')
})
// mentions
let mentions = doc.querySelectorAll('[data-w-e-type="mention"]')
const mentions = doc.querySelectorAll('[data-w-e-type="mention"]')
mentions.forEach((item) => {
const mentionId = JSON.parse(decodeURIComponent(item.getAttribute('data-info') ?? ''))['id']
item.innerHTML = printDataMap.value[mentionId] ?? ''
})
//
let processRecords = doc.querySelectorAll('[data-w-e-type="process-record"]')
let processRecordTable: Element = document.createElement('table')
const processRecords = doc.querySelectorAll('[data-w-e-type="process-record"]')
const processRecordTable: Element = document.createElement('table')
if (processRecords.length > 0) {
// html
processRecordTable.setAttribute('border', '1')
@ -114,15 +423,15 @@ const getPrintTemplateHTML = () => {
headTd.setAttribute('colspan', '2')
headTd.setAttribute('width', 'auto')
headTd.setAttribute('style', 'text-align: center;')
headTd.innerHTML = '流程节点'
headTd.textContent = '流程节点'
headTr.appendChild(headTd)
processRecordTable.appendChild(headTr)
printData.value.tasks.forEach((item) => {
const tr = document.createElement('tr')
const td1 = document.createElement('td')
td1.innerHTML = item.name
td1.textContent = item.name
const td2 = document.createElement('td')
td2.innerHTML = item.description
td2.textContent = item.description
tr.appendChild(td1)
tr.appendChild(td2)
processRecordTable.appendChild(tr)

View File

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

View File

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

View File

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

View File

@ -308,9 +308,9 @@
</template>
</template>
</el-table-column>
<el-table-column label="设备状态" align="center" prop="status">
<el-table-column label="设备状态" align="center" prop="state">
<template #default="scope">
<dict-tag :type="DICT_TYPE.IOT_DEVICE_STATE" :value="scope.row.status" />
<dict-tag :type="DICT_TYPE.IOT_DEVICE_STATE" :value="scope.row.state" />
</template>
</el-table-column>
<el-table-column

View File

@ -56,21 +56,28 @@ watch([urlPrefix, urlPath], () => {
config.value.url = fullUrl.value
})
const syncUrlFields = (url?: string) => {
if (url?.startsWith('https://')) {
urlPrefix.value = 'https://'
urlPath.value = url.substring(8)
} else if (url?.startsWith('http://')) {
urlPrefix.value = 'http://'
urlPath.value = url.substring(7)
} else {
urlPath.value = url ?? ''
}
}
watch(
() => config.value?.url,
(url) => syncUrlFields(url),
{ immediate: true }
)
/** 组件初始化 */
onMounted(() => {
if (!isEmpty(config.value)) {
// URL
if (config.value.url) {
if (config.value.url.startsWith('https://')) {
urlPrefix.value = 'https://'
urlPath.value = config.value.url.substring(8)
} else if (config.value.url.startsWith('http://')) {
urlPrefix.value = 'http://'
urlPath.value = config.value.url.substring(7)
} else {
urlPath.value = config.value.url
}
}
syncUrlFields(config.value.url)
return
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -26,6 +26,7 @@
<!-- 主条件内容配置 -->
<MainConditionInnerConfig
ref="mainConditionRef"
:model-value="trigger"
@update:model-value="updateCondition"
:trigger-type="trigger.type"
@ -118,6 +119,7 @@
</div>
<SubConditionGroupConfig
:ref="(el) => setSubGroupRef(el, subGroupIndex)"
:model-value="subGroup"
@update:model-value="(value) => updateSubGroup(subGroupIndex, value)"
:trigger-type="trigger.type"
@ -184,6 +186,22 @@ const emit = defineEmits<{
}>()
const trigger = useVModel(props, 'modelValue', emit)
const mainConditionRef = ref<InstanceType<typeof MainConditionInnerConfig>>()
type SubConditionGroupExpose = {
validate: () => Promise<boolean>
clearValidate: () => void
}
const subGroupRefs = ref<Record<number, SubConditionGroupExpose>>({})
const setSubGroupRef = (el: unknown, index: number) => {
if (el) {
subGroupRefs.value[index] = el as SubConditionGroupExpose
} else {
delete subGroupRefs.value[index]
}
}
const maxSubGroups = 3 // 3
const maxConditionsPerGroup = 3 // 3
@ -248,4 +266,35 @@ const updateSubGroup = (index: number, subGroup: any) => {
const removeConditionGroup = () => {
trigger.value.conditionGroups = undefined
}
/** 校验主条件及附加子条件组 */
const validate = async (): Promise<boolean> => {
const mainValid = (await mainConditionRef.value?.validate()) ?? true
if (!mainValid) {
return false
}
const groups = trigger.value.conditionGroups
if (!groups?.length) {
return true
}
for (let i = 0; i < groups.length; i++) {
const subGroupRef = subGroupRefs.value[i]
if (subGroupRef?.validate) {
const valid = await subGroupRef.validate()
if (!valid) {
return false
}
}
}
return true
}
const clearValidate = () => {
mainConditionRef.value?.clearValidate()
Object.values(subGroupRefs.value).forEach((ref) => ref.clearValidate?.())
}
defineExpose({ validate, clearValidate })
</script>

View File

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

View File

@ -53,6 +53,7 @@
<div class="p-12px">
<ConditionConfig
:ref="(el) => setConditionRef(el, conditionIndex)"
:model-value="condition"
@update:model-value="(value) => updateCondition(conditionIndex, value)"
:trigger-type="triggerType"
@ -105,6 +106,44 @@ const subGroup = useVModel(props, 'modelValue', emit)
const maxConditions = computed(() => props.maxConditions || 3) //
type ConditionConfigExpose = {
validate: () => Promise<boolean>
clearValidate: () => void
}
const conditionRefs = ref<Record<number, ConditionConfigExpose>>({})
const setConditionRef = (el: unknown, index: number) => {
if (el) {
conditionRefs.value[index] = el as ConditionConfigExpose
} else {
delete conditionRefs.value[index]
}
}
/** 校验组内所有子条件 */
const validate = async (): Promise<boolean> => {
if (!subGroup.value?.length) {
return false
}
for (let i = 0; i < subGroup.value.length; i++) {
const conditionRef = conditionRefs.value[i]
if (conditionRef?.validate) {
const valid = await conditionRef.validate()
if (!valid) {
return false
}
}
}
return true
}
const clearValidate = () => {
Object.values(conditionRefs.value).forEach((ref) => ref.clearValidate?.())
}
defineExpose({ validate, clearValidate })
/** 添加条件 */
const addCondition = async () => {
// subGroup.value

View File

@ -67,6 +67,7 @@
</div>
<SubConditionGroupConfig
:ref="(el) => setSubGroupRef(el, groupIndex)"
:model-value="group"
@update:model-value="(value) => updateConditionGroup(groupIndex, value)"
:trigger-type="IotRuleSceneTriggerTypeEnum.TIMER"
@ -131,6 +132,44 @@ const conditionGroups = useVModel(props, 'modelValue', emit)
const maxGroups = 3 // 3
const maxConditionsPerGroup = 3 // 3
type SubConditionGroupExpose = {
validate: () => Promise<boolean>
clearValidate: () => void
}
const subGroupRefs = ref<Record<number, SubConditionGroupExpose>>({})
const setSubGroupRef = (el: unknown, index: number) => {
if (el) {
subGroupRefs.value[index] = el as SubConditionGroupExpose
} else {
delete subGroupRefs.value[index]
}
}
/** 校验所有附加条件组 */
const validate = async (): Promise<boolean> => {
if (!conditionGroups.value?.length) {
return true
}
for (let i = 0; i < conditionGroups.value.length; i++) {
const subGroupRef = subGroupRefs.value[i]
if (subGroupRef?.validate) {
const valid = await subGroupRef.validate()
if (!valid) {
return false
}
}
}
return true
}
const clearValidate = () => {
Object.values(subGroupRefs.value).forEach((ref) => ref.clearValidate?.())
}
defineExpose({ validate, clearValidate })
/** 添加条件组 */
const addConditionGroup = async () => {
if (!conditionGroups.value) {

View File

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

View File

@ -75,7 +75,7 @@
<el-select
:model-value="action.type"
@update:model-value="(value) => updateActionType(index, value)"
@change="(value) => onActionTypeChange(action, value)"
@change="(value) => onActionTypeChange(action, value, index)"
placeholder="请选择执行类型"
class="w-full"
>
@ -92,6 +92,7 @@
<!-- 设备控制配置 -->
<DeviceControlConfig
v-if="isDeviceAction(action.type)"
:ref="(el) => setDeviceControlRef(el, index)"
:model-value="action"
@update:model-value="(value) => updateAction(index, value)"
/>
@ -99,6 +100,7 @@
<!-- 告警配置 - 只有恢复告警时才显示 -->
<AlertConfig
v-if="action.type === IotRuleSceneActionTypeEnum.ALERT_RECOVER"
:ref="(el) => setAlertConfigRef(el, index)"
:model-value="action.alertConfigId"
@update:model-value="(value) => updateActionAlertConfig(index, value)"
/>
@ -156,6 +158,66 @@ const emit = defineEmits<{
const actions = useVModel(props, 'actions', emit)
type ConfigExpose = {
validate: () => Promise<boolean>
clearValidate: () => void
}
const deviceControlRefs = ref<Record<number, ConfigExpose>>({})
const alertConfigRefs = ref<Record<number, ConfigExpose>>({})
const setDeviceControlRef = (el: unknown, index: number) => {
if (el) {
deviceControlRefs.value[index] = el as ConfigExpose
} else {
delete deviceControlRefs.value[index]
}
}
const setAlertConfigRef = (el: unknown, index: number) => {
if (el) {
alertConfigRefs.value[index] = el as ConfigExpose
} else {
delete alertConfigRefs.value[index]
}
}
/** 校验所有执行器配置 */
const validateAllActions = async (): Promise<boolean> => {
for (let i = 0; i < actions.value.length; i++) {
const action = actions.value[i]
if (isDeviceAction(action.type)) {
const deviceRef = deviceControlRefs.value[i]
if (deviceRef?.validate) {
const valid = await deviceRef.validate()
if (!valid) {
return false
}
}
continue
}
if (action.type === IotRuleSceneActionTypeEnum.ALERT_RECOVER) {
const alertRef = alertConfigRefs.value[i]
if (alertRef?.validate) {
const valid = await alertRef.validate()
if (!valid) {
return false
}
}
}
}
return true
}
const clearAllActionValidate = () => {
Object.values(deviceControlRefs.value).forEach((ref) => ref.clearValidate?.())
Object.values(alertConfigRefs.value).forEach((ref) => ref.clearValidate?.())
}
defineExpose({ validateAllActions, clearAllActionValidate })
/** 获取执行器标签类型(用于 el-tag 的 type 属性) */
const getActionTypeTag = (type: number): 'primary' | 'success' | 'info' | 'warning' | 'danger' => {
const actionTypeTags = {
@ -222,9 +284,8 @@ const removeAction = (index: number) => {
* @param type 执行器类型
*/
const updateActionType = (index: number, type: number) => {
const action = actions.value[index]
onActionTypeChange(action, type) // action.type
action.type = type
actions.value[index].type = type
onActionTypeChange(actions.value[index], type, index)
}
/**
@ -250,7 +311,7 @@ const updateActionAlertConfig = (index: number, alertConfigId?: number) => {
* @param action 执行器对象
* @param type 执行器类型
*/
const onActionTypeChange = (action: Action, type: number) => {
const onActionTypeChange = (action: Action, type: number, index: number) => {
//
if (isDeviceAction(type)) {
//
@ -269,5 +330,10 @@ const onActionTypeChange = (action: Action, type: number) => {
action.params = undefined
action.alertConfigId = undefined
}
nextTick(() => {
deviceControlRefs.value[index]?.clearValidate?.()
alertConfigRefs.value[index]?.clearValidate?.()
})
}
</script>

View File

@ -59,6 +59,7 @@
<!-- 设备触发配置 -->
<DeviceTriggerConfig
v-if="isDeviceTrigger(triggerItem.type)"
:ref="(el) => setDeviceTriggerRef(el, index)"
:model-value="triggerItem"
:index="index"
@update:model-value="(value) => updateTriggerDeviceConfig(index, value)"
@ -93,6 +94,7 @@
<!-- 附加条件组配置 -->
<TimerConditionGroupConfig
:ref="(el) => setTimerConditionRef(el, index)"
:model-value="triggerItem.conditionGroups"
@update:model-value="(value) => updateTriggerConditionGroups(index, value)"
/>
@ -127,6 +129,7 @@ import type { Trigger, TriggerCondition } from '@/api/iot/rule/scene'
import {
getTriggerTypeLabel,
IotRuleSceneTriggerTypeEnum,
IotRuleSceneTriggerConditionParameterOperatorEnum,
isDeviceTrigger
} from '@/views/iot/utils/constants'
@ -143,6 +146,72 @@ const emit = defineEmits<{
const triggers = useVModel(props, 'triggers', emit)
type DeviceTriggerConfigExpose = {
validate: () => Promise<boolean>
clearValidate: () => void
}
const deviceTriggerRefs = ref<Record<number, DeviceTriggerConfigExpose>>({})
type TimerConditionGroupExpose = {
validate: () => Promise<boolean>
clearValidate: () => void
}
const timerConditionRefs = ref<Record<number, TimerConditionGroupExpose>>({})
const setDeviceTriggerRef = (el: unknown, index: number) => {
if (el) {
deviceTriggerRefs.value[index] = el as DeviceTriggerConfigExpose
} else {
delete deviceTriggerRefs.value[index]
}
}
const setTimerConditionRef = (el: unknown, index: number) => {
if (el) {
timerConditionRefs.value[index] = el as TimerConditionGroupExpose
} else {
delete timerConditionRefs.value[index]
}
}
/** 校验所有触发器(主条件 + 附加子条件组) */
const validateAllTriggers = async (): Promise<boolean> => {
for (let i = 0; i < triggers.value.length; i++) {
const triggerItem = triggers.value[i]
if (isDeviceTrigger(triggerItem.type)) {
const deviceConfig = deviceTriggerRefs.value[i]
if (deviceConfig?.validate) {
const valid = await deviceConfig.validate()
if (!valid) {
return false
}
}
continue
}
if (triggerItem.type === IotRuleSceneTriggerTypeEnum.TIMER) {
const timerConfig = timerConditionRefs.value[i]
if (timerConfig?.validate) {
const valid = await timerConfig.validate()
if (!valid) {
return false
}
}
}
}
return true
}
const clearAllTriggerValidate = () => {
Object.values(deviceTriggerRefs.value).forEach((ref) => ref.clearValidate?.())
Object.values(timerConditionRefs.value).forEach((ref) => ref.clearValidate?.())
}
defineExpose({ validateAllTriggers, clearAllTriggerValidate })
/** 获取触发器标签类型(用于 el-tag 的 type 属性) */
const getTriggerTagType = (type: number): 'primary' | 'success' | 'info' | 'warning' | 'danger' => {
if (type === IotRuleSceneTriggerTypeEnum.TIMER) {
@ -158,7 +227,7 @@ const addTrigger = () => {
productId: undefined,
deviceId: undefined,
identifier: undefined,
operator: undefined,
operator: IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value,
value: undefined,
cronExpression: undefined,
conditionGroups: [] //
@ -218,7 +287,7 @@ const updateTriggerConditionGroups = (index: number, conditionGroups: TriggerCon
* @param index 触发器索引
* @param _ 触发器类型未使用
*/
const onTriggerTypeChange = (index: number, _: number) => {
const onTriggerTypeChange = (index: number, type: number) => {
const triggerItem = triggers.value[index]
triggerItem.productId = undefined
triggerItem.deviceId = undefined
@ -227,6 +296,10 @@ const onTriggerTypeChange = (index: number, _: number) => {
triggerItem.value = undefined
triggerItem.cronExpression = undefined
triggerItem.conditionGroups = []
if (type === IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE) {
triggerItem.operator = IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value
}
nextTick(() => deviceTriggerRefs.value[index]?.clearValidate?.())
}
/** 初始化:确保至少有一个触发器 */

View File

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

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

View File

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

View File

@ -28,6 +28,15 @@
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input
v-model="queryParams.email"
class="!w-240px"
clearable
placeholder="请输入邮箱"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="注册时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
@ -90,6 +99,7 @@
</template>
</el-table-column>
<el-table-column align="center" label="手机号" prop="mobile" width="120px" />
<el-table-column align="center" label="邮箱" prop="email" width="180px" />
<el-table-column align="center" label="昵称" prop="nickname" width="80px" />
<el-table-column align="center" label="等级" prop="levelName" width="100px" />
<el-table-column align="center" label="分组" prop="groupName" width="100px" />
@ -227,6 +237,7 @@ const queryParams = reactive({
pageSize: 10,
nickname: null,
mobile: null,
email: null,
loginDate: [],
createTime: [],
tagIds: [],

View File

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

View File

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

View File

@ -40,7 +40,7 @@
:disabled="isHeaderReadonly"
>
<el-option
v-for="dict in getStrDictOptions(DICT_TYPE.MES_WM_MISC_ISSUE_TYPE)"
v-for="dict in getIntDictOptions(DICT_TYPE.MES_WM_MISC_ISSUE_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
@ -120,7 +120,7 @@
</template>
<script setup lang="ts">
import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { WmMiscIssueApi, WmMiscIssueVO } from '@/api/mes/wm/miscissue'
import { AutoCodeRecordApi } from '@/api/mes/md/autocode/record'
import MiscIssueLineList from './MiscIssueLineList.vue'

View File

@ -35,7 +35,7 @@
class="!w-240px"
>
<el-option
v-for="dict in getStrDictOptions(DICT_TYPE.MES_WM_MISC_ISSUE_TYPE)"
v-for="dict in getIntDictOptions(DICT_TYPE.MES_WM_MISC_ISSUE_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
@ -195,7 +195,7 @@
<script setup lang="ts">
import { dateFormatter2 } from '@/utils/formatTime'
import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import download from '@/utils/download'
import { WmMiscIssueApi, WmMiscIssueVO } from '@/api/mes/wm/miscissue'
import MiscIssueForm from './MiscIssueForm.vue'

View File

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

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>