fix(iot): 修复 21 处 bug(P1×15 + P2×6)
经 codex 4 轮复评定稿,antd / ele 两端同步。
P1(场景规则 / 物模型 / Modbus / Redis sink / 路由):
- B7/B8 隐藏路由 path 与 activePath 对齐 vue3 源(产品 / OTA 固件详情)
- B9 移除后端不存在的 deleteSceneRuleList 封装
- B10 物模型 number specs 恢复 min/max/step/unit 校验
- B11 物模型新增枚举项补 dataType: ENUM
- B12 物模型 struct 非空校验绑 fieldPath,array 嵌套显式覆盖
property.dataSpecs.dataSpecsList,确保父表单 validate 触发
- B13 struct 与 input-output-param 编辑回填 cloneDeep,取消不污染原对象
- B14 Modbus Client 模式 ip/port/timeout/retryInterval 改 dependencies 条件必填
- B19 Redis sink 补 dataStructure(默认 Stream)+ Hash/ZSet 条件字段
- B20 仅 ALERT_RECOVER 强校验 alertConfigId,ALERT_TRIGGER 放行
- B21 conditionGroups 递归校验
· 设备状态/属性 param 必填
· CURRENT_TIME 按 operator 区分:TODAY 免、BETWEEN_TIME 双段、其它单段
· 触发器 / 条件 / 执行器 deviceId 改显式 null/undefined 判断,
保留「全部设备 = 0」(后端 action 支持广播执行)
- B22 事件上报条件改回普通 Input,允许标量值或留空
- B23 antd 当前时间条件 :value / @update:value 绑定 + Dayjs 类型 normalize;
归一逻辑抽到 @vben/utils.formatDayjs(packages/@core/base/shared/utils/date.ts),
供所有 app 复用
- B24 设备控制动作切换无条件清依赖,去掉 isInitialized 冗余守卫
- B26 JSON 参数输入先全部校验通过后再写入父表单
P2(产品 / 设备 / 物模型展示 / 数据源):
- B28 产品 deviceType 去默认值,强制用户显式选择
- B30 设备列表 DeviceName 加点击详情 slot
- B31 设备卡片显示备注名称(nickname || deviceName)
- B32 设备详情 hasLocation 改用 != null,合法 0 坐标不再判空
- B41 物模型数据定义展示顺序改为 name-value
- B46 数据源 getData() 剔除仅 UI 用的 identifierLoading 临时字段
pull/348/head
parent
ef57c96b2f
commit
241cf76788
|
|
@ -80,13 +80,6 @@ export function deleteSceneRule(id: number) {
|
|||
return requestClient.delete(`/iot/scene-rule/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 批量删除场景联动规则 */
|
||||
export function deleteSceneRuleList(ids: number[]) {
|
||||
return requestClient.delete('/iot/scene-rule/delete-list', {
|
||||
params: { ids: ids.join(',') },
|
||||
});
|
||||
}
|
||||
|
||||
/** 更新场景联动规则状态 */
|
||||
export function updateSceneRuleStatus(id: number, status: number) {
|
||||
return requestClient.put(`/iot/scene-rule/update-status`, {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ const routes: RouteRecordRaw[] = [
|
|||
},
|
||||
children: [
|
||||
{
|
||||
path: 'product/detail/:id',
|
||||
path: 'product/product/detail/:id',
|
||||
name: 'IoTProductDetail',
|
||||
meta: {
|
||||
title: '产品详情',
|
||||
|
|
@ -30,14 +30,13 @@ const routes: RouteRecordRaw[] = [
|
|||
component: () => import('#/views/iot/device/device/detail/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'ota/firmware/detail/:id',
|
||||
path: 'ota/operation/firmware/detail/:id',
|
||||
name: 'IoTOtaFirmwareDetail',
|
||||
meta: {
|
||||
title: '固件详情',
|
||||
activePath: '/iot/ota',
|
||||
activePath: '/iot/operation/ota/firmware',
|
||||
},
|
||||
component: () =>
|
||||
import('#/views/iot/ota/firmware/detail/index.vue'),
|
||||
component: () => import('#/views/iot/ota/firmware/detail/index.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -276,6 +276,7 @@ export function useGridColumns(): VxeTableGridOptions<IotDeviceApi.Device>['colu
|
|||
field: 'deviceName',
|
||||
title: 'DeviceName',
|
||||
minWidth: 150,
|
||||
slots: { default: 'deviceName' },
|
||||
},
|
||||
{
|
||||
field: 'nickname',
|
||||
|
|
|
|||
|
|
@ -36,9 +36,9 @@ const authInfo = ref<IotDeviceApi.DeviceAuthInfoRespVO>(
|
|||
);
|
||||
const mapDialogRef = ref<InstanceType<typeof MapDialog>>();
|
||||
|
||||
/** 是否有位置信息 */
|
||||
/** 是否有位置信息(合法经纬度 0 不应视为空) */
|
||||
const hasLocation = computed(() => {
|
||||
return !!(props.device.longitude && props.device.latitude);
|
||||
return props.device.longitude != null && props.device.latitude != null;
|
||||
});
|
||||
|
||||
/** 打开地图弹窗 */
|
||||
|
|
|
|||
|
|
@ -58,11 +58,13 @@ const [Form, formApi] = useVbenForm({
|
|||
componentProps: {
|
||||
placeholder: '请输入 Modbus 服务器 IP 地址',
|
||||
},
|
||||
// Client 模式专有字段:必填;Server 模式不显示也不校验
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => isClient.value, // Client 模式专有字段:IP 地址
|
||||
show: () => isClient.value,
|
||||
rules: () =>
|
||||
isClient.value ? z.string().min(1, '请输入 IP 地址') : null,
|
||||
},
|
||||
rules: z.string().min(1, '请输入 IP 地址').optional(),
|
||||
},
|
||||
{
|
||||
fieldName: 'port',
|
||||
|
|
@ -76,9 +78,12 @@ const [Form, formApi] = useVbenForm({
|
|||
},
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => isClient.value, // Client 模式专有字段:端口
|
||||
show: () => isClient.value,
|
||||
rules: () =>
|
||||
isClient.value
|
||||
? z.number({ message: '请输入端口' }).min(1).max(65_535)
|
||||
: null,
|
||||
},
|
||||
rules: z.number().min(1).max(65_535).optional(),
|
||||
defaultValue: 502,
|
||||
},
|
||||
{
|
||||
|
|
@ -106,9 +111,12 @@ const [Form, formApi] = useVbenForm({
|
|||
},
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => isClient.value, // Client 模式专有字段:连接超时
|
||||
show: () => isClient.value,
|
||||
rules: () =>
|
||||
isClient.value
|
||||
? z.number({ message: '请输入连接超时时间' }).min(1000)
|
||||
: null,
|
||||
},
|
||||
rules: z.number().min(1000).optional(),
|
||||
defaultValue: 3000,
|
||||
},
|
||||
{
|
||||
|
|
@ -123,9 +131,12 @@ const [Form, formApi] = useVbenForm({
|
|||
},
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => isClient.value, // Client 模式专有字段:重试间隔
|
||||
show: () => isClient.value,
|
||||
rules: () =>
|
||||
isClient.value
|
||||
? z.number({ message: '请输入重试间隔' }).min(1000)
|
||||
: null,
|
||||
},
|
||||
rules: z.number().min(1000).optional(),
|
||||
defaultValue: 10_000,
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -430,6 +430,11 @@ onMounted(async () => {
|
|||
|
||||
<!-- 列表视图 -->
|
||||
<Grid table-title="设备列表" v-show="viewMode === 'list'">
|
||||
<template #deviceName="{ row }">
|
||||
<a class="cursor-pointer text-primary" @click="openDetail(row.id!)">
|
||||
{{ row.deviceName }}
|
||||
</a>
|
||||
</template>
|
||||
<template #product="{ row }">
|
||||
<a
|
||||
class="cursor-pointer text-primary"
|
||||
|
|
|
|||
|
|
@ -177,13 +177,16 @@ onMounted(() => {
|
|||
</div>
|
||||
<div class="flex items-center text-[13px]">
|
||||
<span class="mr-2 shrink-0 opacity-65 dark:text-white/65">
|
||||
Deviceid
|
||||
备注名称
|
||||
</span>
|
||||
<Tooltip :title="String(item.id)" placement="top">
|
||||
<Tooltip
|
||||
:title="item.nickname || item.deviceName"
|
||||
placement="top"
|
||||
>
|
||||
<span
|
||||
class="inline-block max-w-[150px] cursor-pointer truncate align-middle font-mono text-xs opacity-85 dark:text-white/75"
|
||||
class="inline-block max-w-[150px] cursor-pointer truncate align-middle text-xs opacity-85 dark:text-white/75"
|
||||
>
|
||||
{{ item.id }}
|
||||
{{ item.nickname || item.deviceName }}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -110,7 +110,6 @@ export function useBasicFormSchema(
|
|||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
defaultValue: DeviceTypeEnum.DEVICE,
|
||||
dependencies: {
|
||||
triggerFields: ['id'],
|
||||
componentProps: (values) => ({
|
||||
|
|
|
|||
|
|
@ -176,9 +176,11 @@ function validate() {
|
|||
return Promise.resolve();
|
||||
}
|
||||
|
||||
/** 取当前所有行的值 */
|
||||
/** 取当前所有行的值(剔除 identifierLoading 等仅供 UI 使用的临时字段) */
|
||||
function getData() {
|
||||
return formData.value;
|
||||
return formData.value.map(
|
||||
({ identifierLoading: _identifierLoading, ...rest }) => rest,
|
||||
);
|
||||
}
|
||||
|
||||
/** 设置初始数据 */
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
<script lang="ts" setup>
|
||||
|
||||
import { onMounted } from 'vue';
|
||||
import { computed, onMounted } from 'vue';
|
||||
|
||||
import { isEmpty } from '@vben/utils';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { Form, Input, InputNumber } from 'ant-design-vue';
|
||||
import { Form, Input, InputNumber, Select } from 'ant-design-vue';
|
||||
|
||||
import { IotDataSinkTypeEnum } from '#/api/iot/rule/data/sink';
|
||||
|
||||
|
|
@ -13,8 +13,23 @@ const props = defineProps<{ modelValue: any }>();
|
|||
const emit = defineEmits(['update:modelValue']);
|
||||
const config = useVModel(props, 'modelValue', emit);
|
||||
|
||||
const REDIS_DATA_STRUCTURE_OPTIONS = [
|
||||
{ label: 'Stream', value: 1 },
|
||||
{ label: 'Hash', value: 2 },
|
||||
{ label: 'List', value: 3 },
|
||||
{ label: 'Set', value: 4 },
|
||||
{ label: 'ZSet', value: 5 },
|
||||
{ label: 'String', value: 6 },
|
||||
]; // Redis 数据结构枚举(与后端 IotRedisDataStructureEnum 对应)
|
||||
|
||||
const isHash = computed(() => Number(config.value?.dataStructure) === 2);
|
||||
const isZSet = computed(() => Number(config.value?.dataStructure) === 5);
|
||||
|
||||
onMounted(() => {
|
||||
if (!isEmpty(config.value)) {
|
||||
if (config.value.dataStructure == null) {
|
||||
config.value.dataStructure = 1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
config.value = {
|
||||
|
|
@ -24,6 +39,7 @@ onMounted(() => {
|
|||
password: '',
|
||||
database: 0,
|
||||
topic: '',
|
||||
dataStructure: 1,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
|
@ -89,4 +105,37 @@ onMounted(() => {
|
|||
>
|
||||
<Input v-model:value="config.topic" placeholder="请输入主题" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
:name="['config', 'dataStructure']"
|
||||
:rules="[
|
||||
{ required: true, message: 'Redis 数据结构不能为空', trigger: 'change' },
|
||||
]"
|
||||
label="数据结构"
|
||||
>
|
||||
<Select
|
||||
v-model:value="config.dataStructure"
|
||||
:options="REDIS_DATA_STRUCTURE_OPTIONS"
|
||||
placeholder="请选择 Redis 数据结构"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
v-if="isHash"
|
||||
:name="['config', 'hashField']"
|
||||
label="Hash 字段"
|
||||
>
|
||||
<Input
|
||||
v-model:value="config.hashField"
|
||||
placeholder="留空使用 deviceId 作为 Hash 字段"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
v-if="isZSet"
|
||||
:name="['config', 'scoreField']"
|
||||
label="Score 字段"
|
||||
>
|
||||
<Input
|
||||
v-model:value="config.scoreField"
|
||||
placeholder="留空使用当前时间戳作为 Score"
|
||||
/>
|
||||
</Form.Item>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
<!-- 当前时间条件配置组件 -->
|
||||
<script setup lang="ts">
|
||||
import type { Dayjs } from 'dayjs';
|
||||
|
||||
import type { RuleSceneApi } from '#/api/iot/rule/scene';
|
||||
|
||||
import { computed, watch } from 'vue';
|
||||
|
||||
import { IotRuleSceneTriggerTimeOperatorEnum } from '@vben/constants';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { formatDayjs } from '@vben/utils';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import {
|
||||
|
|
@ -126,11 +129,12 @@ function updateConditionField(field: any, value: any) {
|
|||
* 处理第一个时间值变化
|
||||
* @param value 时间值
|
||||
*/
|
||||
function handleTimeValueChange(value: string) {
|
||||
function handleTimeValueChange(value: Dayjs | null | string) {
|
||||
const normalized = formatDayjs(value);
|
||||
const currentParams = condition.value.param
|
||||
? condition.value.param.split(',')
|
||||
: [];
|
||||
currentParams[0] = value || '';
|
||||
currentParams[0] = normalized;
|
||||
|
||||
// 如果是范围条件,保留第二个值;否则只保留第一个值
|
||||
condition.value.param = needsSecondTimeInput.value
|
||||
|
|
@ -142,11 +146,12 @@ function handleTimeValueChange(value: string) {
|
|||
* 处理第二个时间值变化
|
||||
* @param value 时间值
|
||||
*/
|
||||
function handleTimeValue2Change(value: string) {
|
||||
function handleTimeValue2Change(value: Dayjs | null | string) {
|
||||
const normalized = formatDayjs(value);
|
||||
const currentParams = condition.value.param
|
||||
? condition.value.param.split(',')
|
||||
: [''];
|
||||
currentParams[1] = value || '';
|
||||
currentParams[1] = normalized;
|
||||
condition.value.param = currentParams.slice(0, 2).join(',');
|
||||
}
|
||||
|
||||
|
|
@ -175,8 +180,8 @@ watch(
|
|||
<Col :span="8">
|
||||
<Form.Item label="时间条件" required>
|
||||
<Select
|
||||
:model-value="condition.operator"
|
||||
@update:model-value="
|
||||
:value="condition.operator"
|
||||
@update:value="
|
||||
(value: any) => updateConditionField('operator', value)
|
||||
"
|
||||
placeholder="请选择时间条件"
|
||||
|
|
@ -207,8 +212,8 @@ watch(
|
|||
<Form.Item label="时间值" required>
|
||||
<TimePicker
|
||||
v-if="needsTimeInput"
|
||||
:model-value="timeValue"
|
||||
@update:model-value="handleTimeValueChange"
|
||||
:value="timeValue"
|
||||
@update:value="handleTimeValueChange"
|
||||
placeholder="请选择时间"
|
||||
format="HH:mm:ss"
|
||||
value-format="HH:mm:ss"
|
||||
|
|
@ -216,8 +221,8 @@ watch(
|
|||
/>
|
||||
<DatePicker
|
||||
v-else-if="needsDateInput"
|
||||
:model-value="timeValue"
|
||||
@update:model-value="handleTimeValueChange"
|
||||
:value="timeValue"
|
||||
@update:value="handleTimeValueChange"
|
||||
type="datetime"
|
||||
placeholder="请选择日期时间"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
|
|
@ -233,8 +238,8 @@ watch(
|
|||
<Form.Item label="结束时间" required>
|
||||
<TimePicker
|
||||
v-if="needsTimeInput"
|
||||
:model-value="timeValue2"
|
||||
@update:model-value="handleTimeValue2Change"
|
||||
:value="timeValue2"
|
||||
@update:value="handleTimeValue2Change"
|
||||
placeholder="请选择结束时间"
|
||||
format="HH:mm:ss"
|
||||
value-format="HH:mm:ss"
|
||||
|
|
@ -242,8 +247,8 @@ watch(
|
|||
/>
|
||||
<DatePicker
|
||||
v-else
|
||||
:model-value="timeValue2"
|
||||
@update:model-value="handleTimeValue2Change"
|
||||
:value="timeValue2"
|
||||
@update:value="handleTimeValue2Change"
|
||||
type="datetime"
|
||||
placeholder="请选择结束日期时间"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
|
|
|
|||
|
|
@ -69,18 +69,16 @@ const isServiceInvokeAction = computed(() => {
|
|||
/**
|
||||
* 处理产品变化事件
|
||||
* @param productId 产品 ID
|
||||
*
|
||||
* ProductSelector 只在用户主动切换时 emit change,编辑回填阶段不会触发。
|
||||
*/
|
||||
function handleProductChange(productId?: number) {
|
||||
// 当产品变化时,清空设备选择和参数配置
|
||||
if (action.value.productId !== productId) {
|
||||
action.value.deviceId = undefined;
|
||||
action.value.identifier = undefined; // 清空服务标识符
|
||||
action.value.params = '' as any; // 清空参数,保存为空字符串
|
||||
selectedService.value = null; // 清空选中的服务
|
||||
serviceList.value = []; // 清空服务列表
|
||||
}
|
||||
action.value.deviceId = undefined;
|
||||
action.value.identifier = undefined;
|
||||
action.value.params = '' as any;
|
||||
selectedService.value = null;
|
||||
serviceList.value = [];
|
||||
|
||||
// 加载新产品的物模型属性或服务列表
|
||||
if (productId) {
|
||||
if (isPropertySetAction.value) {
|
||||
loadThingModelProperties(productId);
|
||||
|
|
@ -94,11 +92,8 @@ function handleProductChange(productId?: number) {
|
|||
* 处理设备变化事件
|
||||
* @param deviceId 设备 ID
|
||||
*/
|
||||
function handleDeviceChange(deviceId?: number) {
|
||||
// 当设备变化时,清空参数配置
|
||||
if (action.value.deviceId !== deviceId) {
|
||||
action.value.params = '' as any; // 清空参数,保存为空字符串
|
||||
}
|
||||
function handleDeviceChange(_deviceId?: number) {
|
||||
action.value.params = '' as any;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -257,14 +252,10 @@ function getDefaultValueForParam(param: any) {
|
|||
}
|
||||
}
|
||||
|
||||
const isInitialized = ref(false); // 防止重复初始化的标志
|
||||
|
||||
/**
|
||||
* 初始化组件数据
|
||||
*/
|
||||
async function initializeComponent() {
|
||||
if (isInitialized.value) return;
|
||||
|
||||
const currentAction = action.value;
|
||||
if (!currentAction) return;
|
||||
|
||||
|
|
@ -282,8 +273,6 @@ async function initializeComponent() {
|
|||
// 加载物模型TSL以获取服务信息
|
||||
await loadServiceFromTSL(currentAction.productId, currentAction.identifier);
|
||||
}
|
||||
|
||||
isInitialized.value = true;
|
||||
}
|
||||
|
||||
/** 组件初始化 */
|
||||
|
|
@ -295,9 +284,6 @@ onMounted(() => {
|
|||
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) {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {
|
|||
} from '@vben/constants';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { Col, Form, Row, Select } from 'ant-design-vue';
|
||||
import { Col, Form, Input, Row, Select } from 'ant-design-vue';
|
||||
|
||||
import JsonParamsInput from '../inputs/json-params-input.vue';
|
||||
import ValueInput from '../inputs/value-input.vue';
|
||||
|
|
@ -105,22 +105,6 @@ const serviceConfig = computed(() => {
|
|||
return undefined;
|
||||
});
|
||||
|
||||
// 计算属性:事件配置 - 用于 JsonParamsInput
|
||||
const eventConfig = computed(() => {
|
||||
if (
|
||||
propertyConfig.value &&
|
||||
props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST
|
||||
) {
|
||||
return {
|
||||
event: {
|
||||
name: propertyConfig.value.name || '事件',
|
||||
outputParams: propertyConfig.value.outputParams || [],
|
||||
},
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
/**
|
||||
* 更新条件字段
|
||||
* @param field 字段名
|
||||
|
|
@ -266,15 +250,16 @@ function handlePropertyChange(propertyInfo: any) {
|
|||
:config="serviceConfig as any"
|
||||
placeholder="请输入 JSON 格式的服务参数"
|
||||
/>
|
||||
<!-- 事件上报参数配置 -->
|
||||
<JsonParamsInput
|
||||
<!-- 事件上报参数配置:源项目允许标量值或留空表示事件发生即匹配 -->
|
||||
<Input
|
||||
v-else-if="
|
||||
triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST
|
||||
"
|
||||
v-model="condition.value"
|
||||
type="event"
|
||||
:config="eventConfig as any"
|
||||
placeholder="请输入 JSON 格式的事件参数"
|
||||
:value="condition.value"
|
||||
@update:value="
|
||||
(value) => updateConditionField('value', value)
|
||||
"
|
||||
placeholder="留空则事件发生即匹配"
|
||||
/>
|
||||
<!-- 普通值输入 -->
|
||||
<ValueInput
|
||||
|
|
|
|||
|
|
@ -219,39 +219,42 @@ const emptyMessage = computed(() => {
|
|||
|
||||
/**
|
||||
* 处理参数变化事件
|
||||
*
|
||||
* 注意:必须在所有校验(合法 JSON / 必须是对象 / 必填参数)通过后再回写
|
||||
* localValue,否则父表单仅校验非空时会先写入非法值再提示错误。
|
||||
*/
|
||||
function handleParamsChange() {
|
||||
try {
|
||||
jsonError.value = ''; // 清除之前的错误
|
||||
jsonError.value = '';
|
||||
|
||||
if (paramsJson.value.trim()) {
|
||||
const parsed = JSON.parse(paramsJson.value);
|
||||
localValue.value = paramsJson.value;
|
||||
|
||||
// 额外的参数验证
|
||||
if (typeof parsed !== 'object' || parsed === null) {
|
||||
jsonError.value = JSON_PARAMS_INPUT_CONSTANTS.PARAMS_MUST_BE_OBJECT;
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证必填参数
|
||||
for (const param of paramsList.value) {
|
||||
if (
|
||||
param.required &&
|
||||
(!parsed[param.identifier] || parsed[param.identifier] === '')
|
||||
) {
|
||||
jsonError.value = JSON_PARAMS_INPUT_CONSTANTS.PARAM_REQUIRED_ERROR(
|
||||
param.name,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!paramsJson.value.trim()) {
|
||||
localValue.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证通过
|
||||
jsonError.value = '';
|
||||
const parsed = JSON.parse(paramsJson.value);
|
||||
|
||||
// 必须是对象
|
||||
if (typeof parsed !== 'object' || parsed === null) {
|
||||
jsonError.value = JSON_PARAMS_INPUT_CONSTANTS.PARAMS_MUST_BE_OBJECT;
|
||||
return;
|
||||
}
|
||||
|
||||
// 必填参数校验
|
||||
for (const param of paramsList.value) {
|
||||
if (
|
||||
param.required &&
|
||||
(!parsed[param.identifier] || parsed[param.identifier] === '')
|
||||
) {
|
||||
jsonError.value = JSON_PARAMS_INPUT_CONSTANTS.PARAM_REQUIRED_ERROR(
|
||||
param.name,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 所有校验通过后才回写到父表单
|
||||
localValue.value = paramsJson.value;
|
||||
} catch (error) {
|
||||
jsonError.value = JSON_PARAMS_INPUT_CONSTANTS.JSON_FORMAT_ERROR(
|
||||
error instanceof Error
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import { useVbenDrawer } from '@vben/common-ui';
|
|||
import {
|
||||
CommonStatusEnum,
|
||||
IotRuleSceneActionTypeEnum,
|
||||
IotRuleSceneTriggerConditionTypeEnum,
|
||||
IotRuleSceneTriggerTimeOperatorEnum,
|
||||
IotRuleSceneTriggerTypeEnum,
|
||||
isDeviceTrigger,
|
||||
} from '@vben/constants';
|
||||
|
|
@ -126,7 +128,8 @@ function validateTriggers(_rule: any, value: any, callback: any) {
|
|||
callback(new Error(`触发器 ${i + 1}:产品不能为空`));
|
||||
return;
|
||||
}
|
||||
if (!trigger.deviceId) {
|
||||
// deviceId = 0 表示「全部设备」(DEVICE_SELECTOR_OPTIONS.ALL_DEVICES),是合法值;仅 undefined / null 视为未选
|
||||
if (trigger.deviceId === undefined || trigger.deviceId === null) {
|
||||
callback(new Error(`触发器 ${i + 1}:设备不能为空`));
|
||||
return;
|
||||
}
|
||||
|
|
@ -160,6 +163,89 @@ function validateTriggers(_rule: any, value: any, callback: any) {
|
|||
callback(new Error(`触发器 ${i + 1}:CRON 表达式不能为空`));
|
||||
return;
|
||||
}
|
||||
// 递归校验 conditionGroups(嵌套条件组)
|
||||
if (trigger.conditionGroups?.length) {
|
||||
for (const [gi, group] of trigger.conditionGroups.entries()) {
|
||||
if (!Array.isArray(group) || group.length === 0) {
|
||||
callback(
|
||||
new Error(`触发器 ${i + 1}:条件组 ${gi + 1} 不能为空`),
|
||||
);
|
||||
return;
|
||||
}
|
||||
for (const [ci, condition] of group.entries()) {
|
||||
const prefix = `触发器 ${i + 1} 条件组 ${gi + 1} 条件 ${ci + 1}`;
|
||||
if (!condition.type) {
|
||||
callback(new Error(`${prefix}:条件类型不能为空`));
|
||||
return;
|
||||
}
|
||||
const isDeviceStatus =
|
||||
condition.type ===
|
||||
IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS;
|
||||
const isDeviceProperty =
|
||||
condition.type ===
|
||||
IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY;
|
||||
const isCurrentTime =
|
||||
condition.type ===
|
||||
IotRuleSceneTriggerConditionTypeEnum.CURRENT_TIME;
|
||||
if (isDeviceStatus || isDeviceProperty) {
|
||||
if (!condition.productId) {
|
||||
callback(new Error(`${prefix}:产品不能为空`));
|
||||
return;
|
||||
}
|
||||
// deviceId = 0 表示「全部设备」(DEVICE_SELECTOR_OPTIONS.ALL_DEVICES),是合法值
|
||||
if (
|
||||
condition.deviceId === undefined ||
|
||||
condition.deviceId === null
|
||||
) {
|
||||
callback(new Error(`${prefix}:设备不能为空`));
|
||||
return;
|
||||
}
|
||||
if (isDeviceProperty && !condition.identifier) {
|
||||
callback(new Error(`${prefix}:物模型标识符不能为空`));
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!condition.operator) {
|
||||
callback(new Error(`${prefix}:操作符不能为空`));
|
||||
return;
|
||||
}
|
||||
// 设备状态:param 是状态值(必填);设备属性:param 是比较值(必填)
|
||||
if (
|
||||
(isDeviceStatus || isDeviceProperty) &&
|
||||
(condition.param === undefined ||
|
||||
condition.param === null ||
|
||||
condition.param === '')
|
||||
) {
|
||||
callback(
|
||||
new Error(
|
||||
`${prefix}:${isDeviceStatus ? '设备状态' : '比较值'}不能为空`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
// 当前时间:TODAY 不需要 param;BETWEEN_TIME 需要双段「v1,v2」;其它需要单段
|
||||
if (isCurrentTime) {
|
||||
const op = condition.operator;
|
||||
if (op === IotRuleSceneTriggerTimeOperatorEnum.TODAY.value) {
|
||||
// TODAY 无需 param
|
||||
} else if (
|
||||
op === IotRuleSceneTriggerTimeOperatorEnum.BETWEEN_TIME.value
|
||||
) {
|
||||
const parts = condition.param
|
||||
? String(condition.param).split(',')
|
||||
: [];
|
||||
if (parts.length < 2 || !parts[0] || !parts[1]) {
|
||||
callback(new Error(`${prefix}:起止时间不能为空`));
|
||||
return;
|
||||
}
|
||||
} else if (!condition.param) {
|
||||
callback(new Error(`${prefix}:时间值不能为空`));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
callback();
|
||||
}
|
||||
|
|
@ -183,7 +269,10 @@ function validateActions(_rule: any, value: any, callback: any) {
|
|||
callback(new Error(`执行器 ${i + 1}:产品不能为空`));
|
||||
return;
|
||||
}
|
||||
if (!action.deviceId) {
|
||||
// deviceId = 0 表示「全部设备」(DEVICE_SELECTOR_OPTIONS.ALL_DEVICES);
|
||||
// 后端 IotDevicePropertySetSceneRuleAction / IotDeviceServiceInvokeSceneRuleAction
|
||||
// 均支持广播执行,因此 0 是合法值,仅 undefined / null 视为未选
|
||||
if (action.deviceId === undefined || action.deviceId === null) {
|
||||
callback(new Error(`执行器 ${i + 1}:设备不能为空`));
|
||||
return;
|
||||
}
|
||||
|
|
@ -199,9 +288,9 @@ function validateActions(_rule: any, value: any, callback: any) {
|
|||
return;
|
||||
}
|
||||
}
|
||||
// 仅恢复告警动作需要选择已有告警配置;触发告警动作不需要预选 alertConfigId
|
||||
if (
|
||||
(action.type === IotRuleSceneActionTypeEnum.ALERT_TRIGGER ||
|
||||
action.type === IotRuleSceneActionTypeEnum.ALERT_RECOVER) &&
|
||||
action.type === IotRuleSceneActionTypeEnum.ALERT_RECOVER &&
|
||||
!action.alertConfigId
|
||||
) {
|
||||
callback(new Error(`执行器 ${i + 1}:告警配置不能为空`));
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ const formattedDataSpecsList = computed(() => {
|
|||
return '';
|
||||
}
|
||||
return props.data.property.dataSpecsList
|
||||
.map((item) => `${item.value}-${item.name}`)
|
||||
.map((item) => `${item.name}-${item.value}`)
|
||||
.join('、');
|
||||
});
|
||||
|
||||
|
|
@ -44,8 +44,8 @@ const shortText = computed(() => {
|
|||
}
|
||||
const first = list[0];
|
||||
return list.length > 1
|
||||
? `${first.value}-${first.name} 等 ${list.length} 项`
|
||||
: `${first.value}-${first.name}`;
|
||||
? `${first.name}-${first.value} 等 ${list.length} 项`
|
||||
: `${first.name}-${first.value}`;
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -68,5 +68,6 @@ function handleChange(val: any) {
|
|||
<ThingModelStructDataSpecs
|
||||
v-if="dataSpecs.childDataType === IoTDataSpecsDataTypeEnum.STRUCT"
|
||||
v-model="dataSpecs.dataSpecsList"
|
||||
:field-path="['property', 'dataSpecs', 'dataSpecsList']"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
<script lang="ts" setup>
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import { IoTDataSpecsDataTypeEnum } from '@vben/constants';
|
||||
import { isEmpty } from '@vben/utils';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
|
|
@ -17,7 +18,11 @@ const validateEnumName = buildIdentifierLikeNameValidator('枚举描述');
|
|||
|
||||
/** 添加枚举项 */
|
||||
function addEnum() {
|
||||
dataSpecsList.value.push({ name: '', value: '' } as any);
|
||||
dataSpecsList.value.push({
|
||||
dataType: IoTDataSpecsDataTypeEnum.ENUM,
|
||||
name: '',
|
||||
value: '',
|
||||
} as any);
|
||||
}
|
||||
|
||||
/** 删除枚举项 */
|
||||
|
|
|
|||
|
|
@ -34,24 +34,99 @@ function unitChange(unitSpecs: any) {
|
|||
dataSpecs.value.unitName = unitName;
|
||||
dataSpecs.value.unit = unit;
|
||||
}
|
||||
|
||||
/** 校验最小值 */
|
||||
function validateMin(_rule: any, _value: any, callback: any) {
|
||||
const min = Number(dataSpecs.value.min);
|
||||
const max = Number(dataSpecs.value.max);
|
||||
if (Number.isNaN(min)) {
|
||||
callback(new Error('请输入有效的数值'));
|
||||
return;
|
||||
}
|
||||
if (!Number.isNaN(max) && min >= max) {
|
||||
callback(new Error('最小值必须小于最大值'));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
}
|
||||
|
||||
/** 校验最大值 */
|
||||
function validateMax(_rule: any, _value: any, callback: any) {
|
||||
const min = Number(dataSpecs.value.min);
|
||||
const max = Number(dataSpecs.value.max);
|
||||
if (Number.isNaN(max)) {
|
||||
callback(new Error('请输入有效的数值'));
|
||||
return;
|
||||
}
|
||||
if (!Number.isNaN(min) && max <= min) {
|
||||
callback(new Error('最大值必须大于最小值'));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
}
|
||||
|
||||
/** 校验步长 */
|
||||
function validateStep(_rule: any, _value: any, callback: any) {
|
||||
const step = Number(dataSpecs.value.step);
|
||||
if (Number.isNaN(step)) {
|
||||
callback(new Error('请输入有效的数值'));
|
||||
return;
|
||||
}
|
||||
if (step <= 0) {
|
||||
callback(new Error('步长必须大于 0'));
|
||||
return;
|
||||
}
|
||||
const min = Number(dataSpecs.value.min);
|
||||
const max = Number(dataSpecs.value.max);
|
||||
if (!Number.isNaN(min) && !Number.isNaN(max) && step > max - min) {
|
||||
callback(new Error('步长不能大于最大值与最小值的差值'));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Form.Item label="取值范围">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1">
|
||||
<Form.Item
|
||||
:name="['property', 'dataSpecs', 'min']"
|
||||
:rules="[
|
||||
{ required: true, message: '最小值不能为空', trigger: 'blur' },
|
||||
{ validator: validateMin, trigger: 'blur' },
|
||||
]"
|
||||
class="mb-0 flex-1"
|
||||
>
|
||||
<Input v-model:value="dataSpecs.min" placeholder="请输入最小值" />
|
||||
</div>
|
||||
</Form.Item>
|
||||
<span class="mx-2">~</span>
|
||||
<div class="flex-1">
|
||||
<Form.Item
|
||||
:name="['property', 'dataSpecs', 'max']"
|
||||
:rules="[
|
||||
{ required: true, message: '最大值不能为空', trigger: 'blur' },
|
||||
{ validator: validateMax, trigger: 'blur' },
|
||||
]"
|
||||
class="mb-0 flex-1"
|
||||
>
|
||||
<Input v-model:value="dataSpecs.max" placeholder="请输入最大值" />
|
||||
</div>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</Form.Item>
|
||||
<Form.Item label="步长">
|
||||
<Form.Item
|
||||
:name="['property', 'dataSpecs', 'step']"
|
||||
:rules="[
|
||||
{ required: true, message: '步长不能为空', trigger: 'blur' },
|
||||
{ validator: validateStep, trigger: 'blur' },
|
||||
]"
|
||||
label="步长"
|
||||
>
|
||||
<Input v-model:value="dataSpecs.step" placeholder="请输入步长" />
|
||||
</Form.Item>
|
||||
<Form.Item label="单位">
|
||||
<Form.Item
|
||||
:name="['property', 'dataSpecs', 'unit']"
|
||||
:rules="[{ required: true, message: '请选择单位', trigger: 'change' }]"
|
||||
label="单位"
|
||||
>
|
||||
<Select
|
||||
:value="
|
||||
dataSpecs.unit ? `${dataSpecs.unitName}-${dataSpecs.unit}` : ''
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { onMounted, ref } from 'vue';
|
|||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { IoTDataSpecsDataTypeEnum } from '@vben/constants';
|
||||
import { isEmpty } from '@vben/utils';
|
||||
import { cloneDeep, isEmpty } from '@vben/utils';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { Button, Divider, Form, Input } from 'ant-design-vue';
|
||||
|
|
@ -14,13 +14,33 @@ import { ThingModelFormRules } from '#/api/iot/thingmodel';
|
|||
|
||||
import ThingModelProperty from '../property.vue';
|
||||
|
||||
const props = defineProps<{ modelValue: any }>();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
/** 父表单中 dataSpecsList 的 name 路径,默认 property.dataSpecsList;
|
||||
* array 嵌套 struct 时父级需传 ['property', 'dataSpecs', 'dataSpecsList'],
|
||||
* 否则父表单 validate() 无法定位该字段,非空校验不会触发。 */
|
||||
fieldPath?: string[];
|
||||
modelValue: any;
|
||||
}>(),
|
||||
{
|
||||
fieldPath: () => ['property', 'dataSpecsList'],
|
||||
},
|
||||
);
|
||||
const emits = defineEmits(['update:modelValue']);
|
||||
const dataSpecsList = useVModel(props, 'modelValue', emits) as Ref<any[]>;
|
||||
|
||||
const structFormRef = ref();
|
||||
const formData = ref<any>(buildEmptyFormData());
|
||||
|
||||
/** 校验结构体属性对象非空 */
|
||||
function validateStructSpecsList(_rule: any, _value: any, callback: any) {
|
||||
if (isEmpty(dataSpecsList.value)) {
|
||||
callback(new Error('请至少添加一个结构体属性对象'));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
}
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
try {
|
||||
|
|
@ -64,14 +84,15 @@ const [Modal, modalApi] = useVbenModal({
|
|||
if (isEmpty(data)) {
|
||||
return;
|
||||
}
|
||||
// 编辑回显时 cloneDeep,避免弹窗 v-model 改到原始对象(用户取消时不污染外层 dataSpecsList)
|
||||
formData.value = {
|
||||
identifier: data.identifier ?? '',
|
||||
name: data.name ?? '',
|
||||
description: data.description ?? '',
|
||||
property: {
|
||||
dataType: data.childDataType ?? IoTDataSpecsDataTypeEnum.INT,
|
||||
dataSpecs: data.dataSpecs ?? {},
|
||||
dataSpecsList: data.dataSpecsList ?? [],
|
||||
dataSpecs: data.dataSpecs ? cloneDeep(data.dataSpecs) : {},
|
||||
dataSpecsList: data.dataSpecsList ? cloneDeep(data.dataSpecsList) : [],
|
||||
},
|
||||
};
|
||||
},
|
||||
|
|
@ -109,7 +130,11 @@ onMounted(() => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<Form.Item label="属性对象">
|
||||
<Form.Item
|
||||
:name="fieldPath"
|
||||
:rules="[{ validator: validateStructSpecsList, trigger: 'change' }]"
|
||||
label="属性对象"
|
||||
>
|
||||
<div
|
||||
v-for="(item, index) in dataSpecsList"
|
||||
:key="index"
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { ref } from 'vue';
|
|||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { IoTDataSpecsDataTypeEnum } from '@vben/constants';
|
||||
import { isEmpty } from '@vben/utils';
|
||||
import { cloneDeep, isEmpty } from '@vben/utils';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { Button, Divider, Form, Input } from 'ant-design-vue';
|
||||
|
|
@ -72,15 +72,15 @@ const [Modal, modalApi] = useVbenModal({
|
|||
if (isEmpty(data)) {
|
||||
return;
|
||||
}
|
||||
// 设置到 values
|
||||
// 编辑回显时 cloneDeep,避免弹窗 v-model 改到原始对象(用户取消时不污染外层 thingModelParams)
|
||||
formData.value = {
|
||||
identifier: data.identifier ?? '',
|
||||
name: data.name ?? '',
|
||||
description: data.description ?? '',
|
||||
property: {
|
||||
dataType: data.dataType ?? IoTDataSpecsDataTypeEnum.INT,
|
||||
dataSpecs: data.dataSpecs ?? {},
|
||||
dataSpecsList: data.dataSpecsList ?? [],
|
||||
dataSpecs: data.dataSpecs ? cloneDeep(data.dataSpecs) : {},
|
||||
dataSpecsList: data.dataSpecsList ? cloneDeep(data.dataSpecsList) : [],
|
||||
},
|
||||
};
|
||||
},
|
||||
|
|
|
|||
|
|
@ -130,10 +130,8 @@ export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
|
|||
onChange: async () => {
|
||||
await formApi?.setFieldValue('areaId', undefined);
|
||||
},
|
||||
options: list.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
})),
|
||||
fieldNames: { label: 'name', value: 'id' },
|
||||
options: list,
|
||||
placeholder: '请选择库区',
|
||||
};
|
||||
},
|
||||
|
|
@ -152,10 +150,8 @@ export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
|
|||
: [];
|
||||
return {
|
||||
allowClear: true,
|
||||
options: list.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
})),
|
||||
fieldNames: { label: 'name', value: 'id' },
|
||||
options: list,
|
||||
placeholder: '请选择库位',
|
||||
};
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,3 +1,13 @@
|
|||
/** MES 单据状态常量 */
|
||||
export const MesOrderStatusConstants = {
|
||||
DRAFT: 0,
|
||||
CONFIRMED: 1,
|
||||
APPROVING: 2,
|
||||
APPROVED: 3,
|
||||
FINISHED: 4,
|
||||
CANCELLED: 5,
|
||||
} as const;
|
||||
|
||||
/** MES 物料/产品标识枚举 */
|
||||
export const MesItemOrProductEnum = {
|
||||
ITEM: {
|
||||
|
|
@ -10,14 +20,121 @@ export const MesItemOrProductEnum = {
|
|||
},
|
||||
} as const;
|
||||
|
||||
/** MES 工具状态枚举 */
|
||||
export const MesToolStatusEnum = {
|
||||
STORE: 1,
|
||||
ISSUE: 2,
|
||||
REPAIR: 3,
|
||||
SCRAP: 4,
|
||||
} as const;
|
||||
|
||||
/** MES 保养维护类型枚举 */
|
||||
export const MesMaintenTypeEnum = {
|
||||
REGULAR: 1,
|
||||
USAGE: 2,
|
||||
} as const;
|
||||
|
||||
/** MES 设备状态枚举 */
|
||||
export const MesDvMachineryStatusEnum = {
|
||||
STOP: 1,
|
||||
PRODUCING: 2,
|
||||
MAINTENANCE: 3,
|
||||
} as const;
|
||||
|
||||
/** MES 假期类型枚举 */
|
||||
export const HolidayType = {
|
||||
WORKDAY: 1,
|
||||
HOLIDAY: 2,
|
||||
} as const;
|
||||
|
||||
/** MES 排班计划状态枚举 */
|
||||
export const MesCalPlanStatusEnum = {
|
||||
PREPARE: 0,
|
||||
CONFIRMED: 1,
|
||||
} as const;
|
||||
|
||||
/** MES 轮班方式枚举 */
|
||||
export const MesCalShiftTypeEnum = {
|
||||
SINGLE: 1,
|
||||
TWO: 2,
|
||||
THREE: 3,
|
||||
} as const;
|
||||
|
||||
/** MES 倒班方式枚举 */
|
||||
export const MesCalShiftMethodEnum = {
|
||||
QUARTER: 1,
|
||||
MONTH: 2,
|
||||
WEEK: 3,
|
||||
DAY: 4,
|
||||
} as const;
|
||||
|
||||
/** MES 点检保养项目类型枚举 */
|
||||
export const MesDvSubjectTypeEnum = {
|
||||
CHECK: 1,
|
||||
MAINTENANCE: 2,
|
||||
} as const;
|
||||
|
||||
/** MES 点检保养方案状态枚举 */
|
||||
export const MesDvCheckPlanStatusEnum = {
|
||||
PREPARE: 0,
|
||||
ENABLED: 1,
|
||||
} as const;
|
||||
|
||||
/** MES 设备保养记录状态枚举 */
|
||||
export const MesDvMaintenRecordStatusEnum = {
|
||||
PREPARE: MesOrderStatusConstants.DRAFT,
|
||||
SUBMITTED: MesOrderStatusConstants.FINISHED,
|
||||
} as const;
|
||||
|
||||
/** MES 设备保养明细结果枚举 */
|
||||
export const MesDvMaintenStatusEnum = {
|
||||
NORMAL: 1,
|
||||
ABNORMAL: 2,
|
||||
} as const;
|
||||
|
||||
/** MES 设备点检记录状态枚举 */
|
||||
export const MesDvCheckRecordStatusEnum = {
|
||||
DRAFT: 10,
|
||||
FINISHED: 20,
|
||||
} as const;
|
||||
|
||||
/** MES 设备点检结果枚举 */
|
||||
export const MesDvCheckResultEnum = {
|
||||
NORMAL: 1,
|
||||
ABNORMAL: 2,
|
||||
} as const;
|
||||
|
||||
/** MES 维修工单状态枚举 */
|
||||
export const MesDvRepairStatusEnum = {
|
||||
PREPARE: MesOrderStatusConstants.DRAFT,
|
||||
CONFIRMED: MesOrderStatusConstants.CONFIRMED,
|
||||
APPROVING: MesOrderStatusConstants.APPROVING,
|
||||
FINISHED: MesOrderStatusConstants.FINISHED,
|
||||
} as const;
|
||||
|
||||
/** MES 维修结果枚举 */
|
||||
export const MesDvRepairResultEnum = {
|
||||
PASS: 1,
|
||||
FAIL: 2,
|
||||
} as const;
|
||||
|
||||
/** MES 自动编码规则 Code 枚举 */
|
||||
export const MesAutoCodeRuleCode = {
|
||||
CAL_PLAN_CODE: 'CAL_PLAN_CODE',
|
||||
CAL_TEAM_CODE: 'CAL_TEAM_CODE',
|
||||
DV_CHECK_PLAN_CODE: 'DV_CHECK_PLAN_CODE',
|
||||
DV_MACHINERY_TYPE_CODE: 'DV_MACHINERY_TYPE_CODE',
|
||||
DV_MACHINERY_CODE: 'DV_MACHINERY_CODE',
|
||||
DV_REPAIR_CODE: 'DV_REPAIR_CODE',
|
||||
DV_SUBJECT_CODE: 'DV_SUBJECT_CODE',
|
||||
MD_CLIENT_CODE: 'MD_CLIENT_CODE',
|
||||
MD_ITEM_TYPE_CODE: 'MD_ITEM_TYPE_CODE',
|
||||
MD_ITEM_CODE: 'MD_ITEM_CODE',
|
||||
MD_VENDOR_CODE: 'MD_VENDOR_CODE',
|
||||
MD_WORKSTATION_CODE: 'MD_WORKSTATION_CODE',
|
||||
MD_WORKSHOP_CODE: 'MD_WORKSHOP_CODE',
|
||||
TM_TOOL_TYPE_CODE: 'TM_TOOL_TYPE_CODE',
|
||||
TM_TOOL_CODE: 'TM_TOOL_CODE',
|
||||
} as const;
|
||||
|
||||
/** MES 编码规则分段类型枚举 */
|
||||
|
|
|
|||
|
|
@ -81,13 +81,6 @@ export function deleteSceneRule(id: number) {
|
|||
return requestClient.delete(`/iot/scene-rule/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 批量删除场景联动规则 */
|
||||
export function deleteSceneRuleList(ids: number[]) {
|
||||
return requestClient.delete('/iot/scene-rule/delete-list', {
|
||||
params: { ids: ids.join(',') },
|
||||
});
|
||||
}
|
||||
|
||||
/** 更新场景联动规则状态 */
|
||||
export function updateSceneRuleStatus(id: number, status: number) {
|
||||
return requestClient.put(`/iot/scene-rule/update-status`, {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ const routes: RouteRecordRaw[] = [
|
|||
},
|
||||
children: [
|
||||
{
|
||||
path: 'product/detail/:id',
|
||||
path: 'product/product/detail/:id',
|
||||
name: 'IoTProductDetail',
|
||||
meta: {
|
||||
title: '产品详情',
|
||||
|
|
@ -30,14 +30,13 @@ const routes: RouteRecordRaw[] = [
|
|||
component: () => import('#/views/iot/device/device/detail/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'ota/firmware/detail/:id',
|
||||
path: 'ota/operation/firmware/detail/:id',
|
||||
name: 'IoTOtaFirmwareDetail',
|
||||
meta: {
|
||||
title: '固件详情',
|
||||
activePath: '/iot/ota',
|
||||
activePath: '/iot/operation/ota/firmware',
|
||||
},
|
||||
component: () =>
|
||||
import('#/views/iot/ota/firmware/detail/index.vue'),
|
||||
component: () => import('#/views/iot/ota/firmware/detail/index.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -276,6 +276,7 @@ export function useGridColumns(): VxeTableGridOptions<IotDeviceApi.Device>['colu
|
|||
field: 'deviceName',
|
||||
title: 'DeviceName',
|
||||
minWidth: 150,
|
||||
slots: { default: 'deviceName' },
|
||||
},
|
||||
{
|
||||
field: 'nickname',
|
||||
|
|
|
|||
|
|
@ -38,9 +38,9 @@ const authInfo = ref<IotDeviceApi.DeviceAuthInfoRespVO>(
|
|||
);
|
||||
const mapDialogRef = ref<InstanceType<typeof MapDialog>>();
|
||||
|
||||
/** 是否有位置信息 */
|
||||
/** 是否有位置信息(合法经纬度 0 不应视为空) */
|
||||
const hasLocation = computed(() => {
|
||||
return !!(props.device.longitude && props.device.latitude);
|
||||
return props.device.longitude != null && props.device.latitude != null;
|
||||
});
|
||||
|
||||
/** 打开地图弹窗 */
|
||||
|
|
|
|||
|
|
@ -58,11 +58,13 @@ const [Form, formApi] = useVbenForm({
|
|||
componentProps: {
|
||||
placeholder: '请输入 Modbus 服务器 IP 地址',
|
||||
},
|
||||
// Client 模式专有字段:必填;Server 模式不显示也不校验
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => isClient.value, // Client 模式专有字段:IP 地址
|
||||
show: () => isClient.value,
|
||||
rules: () =>
|
||||
isClient.value ? z.string().min(1, '请输入 IP 地址') : null,
|
||||
},
|
||||
rules: z.string().min(1, '请输入 IP 地址').optional(),
|
||||
},
|
||||
{
|
||||
fieldName: 'port',
|
||||
|
|
@ -76,9 +78,12 @@ const [Form, formApi] = useVbenForm({
|
|||
},
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => isClient.value, // Client 模式专有字段:端口
|
||||
show: () => isClient.value,
|
||||
rules: () =>
|
||||
isClient.value
|
||||
? z.number({ message: '请输入端口' }).min(1).max(65_535)
|
||||
: null,
|
||||
},
|
||||
rules: z.number().min(1).max(65_535).optional(),
|
||||
defaultValue: 502,
|
||||
},
|
||||
{
|
||||
|
|
@ -106,9 +111,12 @@ const [Form, formApi] = useVbenForm({
|
|||
},
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => isClient.value, // Client 模式专有字段:连接超时
|
||||
show: () => isClient.value,
|
||||
rules: () =>
|
||||
isClient.value
|
||||
? z.number({ message: '请输入连接超时时间' }).min(1000)
|
||||
: null,
|
||||
},
|
||||
rules: z.number().min(1000).optional(),
|
||||
defaultValue: 3000,
|
||||
},
|
||||
{
|
||||
|
|
@ -123,9 +131,12 @@ const [Form, formApi] = useVbenForm({
|
|||
},
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => isClient.value, // Client 模式专有字段:重试间隔
|
||||
show: () => isClient.value,
|
||||
rules: () =>
|
||||
isClient.value
|
||||
? z.number({ message: '请输入重试间隔' }).min(1000)
|
||||
: null,
|
||||
},
|
||||
rules: z.number().min(1000).optional(),
|
||||
defaultValue: 10_000,
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -424,6 +424,11 @@ onMounted(async () => {
|
|||
|
||||
<!-- 列表视图 -->
|
||||
<Grid table-title="设备列表" v-show="viewMode === 'list'">
|
||||
<template #deviceName="{ row }">
|
||||
<a class="cursor-pointer text-primary" @click="openDetail(row.id!)">
|
||||
{{ row.deviceName }}
|
||||
</a>
|
||||
</template>
|
||||
<template #product="{ row }">
|
||||
<a
|
||||
class="cursor-pointer text-[var(--el-color-primary)]"
|
||||
|
|
|
|||
|
|
@ -173,13 +173,16 @@ onMounted(() => {
|
|||
</div>
|
||||
<div class="flex items-center text-[13px]">
|
||||
<span class="mr-2 shrink-0 opacity-65 dark:text-white/65">
|
||||
Deviceid
|
||||
备注名称
|
||||
</span>
|
||||
<ElTooltip :content="String(item.id)" placement="top">
|
||||
<ElTooltip
|
||||
:content="item.nickname || item.deviceName"
|
||||
placement="top"
|
||||
>
|
||||
<span
|
||||
class="inline-block max-w-[150px] cursor-pointer truncate align-middle font-mono text-xs opacity-85 dark:text-white/75"
|
||||
class="inline-block max-w-[150px] cursor-pointer truncate align-middle text-xs opacity-85 dark:text-white/75"
|
||||
>
|
||||
{{ item.id }}
|
||||
{{ item.nickname || item.deviceName }}
|
||||
</span>
|
||||
</ElTooltip>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -107,7 +107,6 @@ export function useBasicFormSchema(
|
|||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE, 'number'),
|
||||
},
|
||||
defaultValue: DeviceTypeEnum.DEVICE,
|
||||
dependencies: {
|
||||
triggerFields: ['id'],
|
||||
componentProps: (values) => ({
|
||||
|
|
|
|||
|
|
@ -176,9 +176,11 @@ function validate() {
|
|||
return Promise.resolve();
|
||||
}
|
||||
|
||||
/** 取当前所有行的值 */
|
||||
/** 取当前所有行的值(剔除 identifierLoading 等仅供 UI 使用的临时字段) */
|
||||
function getData() {
|
||||
return formData.value;
|
||||
return formData.value.map(
|
||||
({ identifierLoading: _identifierLoading, ...rest }) => rest,
|
||||
);
|
||||
}
|
||||
|
||||
/** 设置初始数据 */
|
||||
|
|
|
|||
|
|
@ -1,11 +1,17 @@
|
|||
<script lang="ts" setup>
|
||||
|
||||
import { onMounted } from 'vue';
|
||||
import { computed, onMounted } from 'vue';
|
||||
|
||||
import { isEmpty } from '@vben/utils';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { ElFormItem, ElInput, ElInputNumber } from 'element-plus';
|
||||
import {
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElInputNumber,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
} from 'element-plus';
|
||||
|
||||
import { IotDataSinkTypeEnum } from '#/api/iot/rule/data/sink';
|
||||
|
||||
|
|
@ -13,8 +19,23 @@ const props = defineProps<{ modelValue: any }>();
|
|||
const emit = defineEmits(['update:modelValue']);
|
||||
const config = useVModel(props, 'modelValue', emit);
|
||||
|
||||
const REDIS_DATA_STRUCTURE_OPTIONS = [
|
||||
{ label: 'Stream', value: 1 },
|
||||
{ label: 'Hash', value: 2 },
|
||||
{ label: 'List', value: 3 },
|
||||
{ label: 'Set', value: 4 },
|
||||
{ label: 'ZSet', value: 5 },
|
||||
{ label: 'String', value: 6 },
|
||||
]; // Redis 数据结构枚举(与后端 IotRedisDataStructureEnum 对应)
|
||||
|
||||
const isHash = computed(() => Number(config.value?.dataStructure) === 2);
|
||||
const isZSet = computed(() => Number(config.value?.dataStructure) === 5);
|
||||
|
||||
onMounted(() => {
|
||||
if (!isEmpty(config.value)) {
|
||||
if (config.value.dataStructure == null) {
|
||||
config.value.dataStructure = 1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
config.value = {
|
||||
|
|
@ -24,6 +45,7 @@ onMounted(() => {
|
|||
password: '',
|
||||
database: 0,
|
||||
topic: '',
|
||||
dataStructure: 1,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
|
@ -94,4 +116,44 @@ onMounted(() => {
|
|||
>
|
||||
<ElInput v-model="config.topic" placeholder="请输入主题" />
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
prop="config.dataStructure"
|
||||
:rules="[
|
||||
{ required: true, message: 'Redis 数据结构不能为空', trigger: 'change' },
|
||||
]"
|
||||
label="数据结构"
|
||||
>
|
||||
<ElSelect
|
||||
v-model="config.dataStructure"
|
||||
placeholder="请选择 Redis 数据结构"
|
||||
class="w-full"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in REDIS_DATA_STRUCTURE_OPTIONS"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
v-if="isHash"
|
||||
prop="config.hashField"
|
||||
label="Hash 字段"
|
||||
>
|
||||
<ElInput
|
||||
v-model="config.hashField"
|
||||
placeholder="留空使用 deviceId 作为 Hash 字段"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
v-if="isZSet"
|
||||
prop="config.scoreField"
|
||||
label="Score 字段"
|
||||
>
|
||||
<ElInput
|
||||
v-model="config.scoreField"
|
||||
placeholder="留空使用当前时间戳作为 Score"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -76,18 +76,16 @@ const isServiceInvokeAction = computed(() => {
|
|||
/**
|
||||
* 处理产品变化事件
|
||||
* @param productId 产品 ID
|
||||
*
|
||||
* ProductSelector 只在用户主动切换时 emit change,编辑回填阶段不会触发。
|
||||
*/
|
||||
function 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.deviceId = undefined;
|
||||
action.value.identifier = undefined;
|
||||
action.value.params = '';
|
||||
selectedService.value = null;
|
||||
serviceList.value = [];
|
||||
|
||||
// 加载新产品的物模型属性或服务列表
|
||||
if (productId) {
|
||||
if (isPropertySetAction.value) {
|
||||
loadThingModelProperties(productId);
|
||||
|
|
@ -101,11 +99,8 @@ function handleProductChange(productId?: number) {
|
|||
* 处理设备变化事件
|
||||
* @param deviceId 设备 ID
|
||||
*/
|
||||
function handleDeviceChange(deviceId?: number) {
|
||||
// 当设备变化时,清空参数配置
|
||||
if (action.value.deviceId !== deviceId) {
|
||||
action.value.params = ''; // 清空参数,保存为空字符串
|
||||
}
|
||||
function handleDeviceChange(_deviceId?: number) {
|
||||
action.value.params = '';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -270,14 +265,10 @@ function getDefaultValueForParam(param: any) {
|
|||
}
|
||||
}
|
||||
|
||||
const isInitialized = ref(false); // 防止重复初始化的标志
|
||||
|
||||
/**
|
||||
* 初始化组件数据
|
||||
*/
|
||||
async function initializeComponent() {
|
||||
if (isInitialized.value) return;
|
||||
|
||||
const currentAction = action.value;
|
||||
if (!currentAction) return;
|
||||
|
||||
|
|
@ -295,8 +286,6 @@ async function initializeComponent() {
|
|||
// 加载物模型TSL以获取服务信息
|
||||
await loadServiceFromTSL(currentAction.productId, currentAction.identifier);
|
||||
}
|
||||
|
||||
isInitialized.value = true;
|
||||
}
|
||||
|
||||
/** 组件初始化 */
|
||||
|
|
@ -308,9 +297,6 @@ onMounted(() => {
|
|||
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) {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,14 @@ import {
|
|||
} from '@vben/constants';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { ElCol, ElFormItem, ElOption, ElRow, ElSelect } from 'element-plus';
|
||||
import {
|
||||
ElCol,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElOption,
|
||||
ElRow,
|
||||
ElSelect,
|
||||
} from 'element-plus';
|
||||
|
||||
import JsonParamsInput from '../inputs/json-params-input.vue';
|
||||
import ValueInput from '../inputs/value-input.vue';
|
||||
|
|
@ -105,22 +112,6 @@ const serviceConfig = computed(() => {
|
|||
return undefined;
|
||||
});
|
||||
|
||||
// 计算属性:事件配置 - 用于 JsonParamsInput
|
||||
const eventConfig = computed(() => {
|
||||
if (
|
||||
propertyConfig.value &&
|
||||
props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST
|
||||
) {
|
||||
return {
|
||||
event: {
|
||||
name: propertyConfig.value.name || '事件',
|
||||
outputParams: propertyConfig.value.outputParams || [],
|
||||
},
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
/**
|
||||
* 更新条件字段
|
||||
* @param field 字段名
|
||||
|
|
@ -265,15 +256,16 @@ function handlePropertyChange(propertyInfo: any) {
|
|||
:config="serviceConfig as any"
|
||||
placeholder="请输入 JSON 格式的服务参数"
|
||||
/>
|
||||
<!-- 事件上报参数配置 -->
|
||||
<JsonParamsInput
|
||||
<!-- 事件上报参数配置:源项目允许标量值或留空表示事件发生即匹配 -->
|
||||
<ElInput
|
||||
v-else-if="
|
||||
triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST
|
||||
"
|
||||
v-model="condition.value"
|
||||
type="event"
|
||||
:config="eventConfig as any"
|
||||
placeholder="请输入 JSON 格式的事件参数"
|
||||
:model-value="condition.value"
|
||||
@update:model-value="
|
||||
(value: any) => updateConditionField('value', value)
|
||||
"
|
||||
placeholder="留空则事件发生即匹配"
|
||||
/>
|
||||
<!-- 普通值输入 -->
|
||||
<ValueInput
|
||||
|
|
|
|||
|
|
@ -219,39 +219,42 @@ const emptyMessage = computed(() => {
|
|||
|
||||
/**
|
||||
* 处理参数变化事件
|
||||
*
|
||||
* 注意:必须在所有校验(合法 JSON / 必须是对象 / 必填参数)通过后再回写
|
||||
* localValue,否则父表单仅校验非空时会先写入非法值再提示错误。
|
||||
*/
|
||||
function handleParamsChange() {
|
||||
try {
|
||||
jsonError.value = ''; // 清除之前的错误
|
||||
jsonError.value = '';
|
||||
|
||||
if (paramsJson.value.trim()) {
|
||||
const parsed = JSON.parse(paramsJson.value);
|
||||
localValue.value = paramsJson.value;
|
||||
|
||||
// 额外的参数验证
|
||||
if (typeof parsed !== 'object' || parsed === null) {
|
||||
jsonError.value = JSON_PARAMS_INPUT_CONSTANTS.PARAMS_MUST_BE_OBJECT;
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证必填参数
|
||||
for (const param of paramsList.value) {
|
||||
if (
|
||||
param.required &&
|
||||
(!parsed[param.identifier] || parsed[param.identifier] === '')
|
||||
) {
|
||||
jsonError.value = JSON_PARAMS_INPUT_CONSTANTS.PARAM_REQUIRED_ERROR(
|
||||
param.name,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!paramsJson.value.trim()) {
|
||||
localValue.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证通过
|
||||
jsonError.value = '';
|
||||
const parsed = JSON.parse(paramsJson.value);
|
||||
|
||||
// 必须是对象
|
||||
if (typeof parsed !== 'object' || parsed === null) {
|
||||
jsonError.value = JSON_PARAMS_INPUT_CONSTANTS.PARAMS_MUST_BE_OBJECT;
|
||||
return;
|
||||
}
|
||||
|
||||
// 必填参数校验
|
||||
for (const param of paramsList.value) {
|
||||
if (
|
||||
param.required &&
|
||||
(!parsed[param.identifier] || parsed[param.identifier] === '')
|
||||
) {
|
||||
jsonError.value = JSON_PARAMS_INPUT_CONSTANTS.PARAM_REQUIRED_ERROR(
|
||||
param.name,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 所有校验通过后才回写到父表单
|
||||
localValue.value = paramsJson.value;
|
||||
} catch (error) {
|
||||
jsonError.value = JSON_PARAMS_INPUT_CONSTANTS.JSON_FORMAT_ERROR(
|
||||
error instanceof Error
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import { useVbenDrawer } from '@vben/common-ui';
|
|||
import {
|
||||
CommonStatusEnum,
|
||||
IotRuleSceneActionTypeEnum,
|
||||
IotRuleSceneTriggerConditionTypeEnum,
|
||||
IotRuleSceneTriggerTimeOperatorEnum,
|
||||
IotRuleSceneTriggerTypeEnum,
|
||||
isDeviceTrigger,
|
||||
} from '@vben/constants';
|
||||
|
|
@ -126,7 +128,8 @@ function validateTriggers(_rule: any, value: any, callback: any) {
|
|||
callback(new Error(`触发器 ${i + 1}:产品不能为空`));
|
||||
return;
|
||||
}
|
||||
if (!trigger.deviceId) {
|
||||
// deviceId = 0 表示「全部设备」(DEVICE_SELECTOR_OPTIONS.ALL_DEVICES),是合法值;仅 undefined / null 视为未选
|
||||
if (trigger.deviceId === undefined || trigger.deviceId === null) {
|
||||
callback(new Error(`触发器 ${i + 1}:设备不能为空`));
|
||||
return;
|
||||
}
|
||||
|
|
@ -160,6 +163,89 @@ function validateTriggers(_rule: any, value: any, callback: any) {
|
|||
callback(new Error(`触发器 ${i + 1}:CRON 表达式不能为空`));
|
||||
return;
|
||||
}
|
||||
// 递归校验 conditionGroups(嵌套条件组)
|
||||
if (trigger.conditionGroups?.length) {
|
||||
for (const [gi, group] of trigger.conditionGroups.entries()) {
|
||||
if (!Array.isArray(group) || group.length === 0) {
|
||||
callback(
|
||||
new Error(`触发器 ${i + 1}:条件组 ${gi + 1} 不能为空`),
|
||||
);
|
||||
return;
|
||||
}
|
||||
for (const [ci, condition] of group.entries()) {
|
||||
const prefix = `触发器 ${i + 1} 条件组 ${gi + 1} 条件 ${ci + 1}`;
|
||||
if (!condition.type) {
|
||||
callback(new Error(`${prefix}:条件类型不能为空`));
|
||||
return;
|
||||
}
|
||||
const isDeviceStatus =
|
||||
condition.type ===
|
||||
IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS;
|
||||
const isDeviceProperty =
|
||||
condition.type ===
|
||||
IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY;
|
||||
const isCurrentTime =
|
||||
condition.type ===
|
||||
IotRuleSceneTriggerConditionTypeEnum.CURRENT_TIME;
|
||||
if (isDeviceStatus || isDeviceProperty) {
|
||||
if (!condition.productId) {
|
||||
callback(new Error(`${prefix}:产品不能为空`));
|
||||
return;
|
||||
}
|
||||
// deviceId = 0 表示「全部设备」(DEVICE_SELECTOR_OPTIONS.ALL_DEVICES),是合法值
|
||||
if (
|
||||
condition.deviceId === undefined ||
|
||||
condition.deviceId === null
|
||||
) {
|
||||
callback(new Error(`${prefix}:设备不能为空`));
|
||||
return;
|
||||
}
|
||||
if (isDeviceProperty && !condition.identifier) {
|
||||
callback(new Error(`${prefix}:物模型标识符不能为空`));
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!condition.operator) {
|
||||
callback(new Error(`${prefix}:操作符不能为空`));
|
||||
return;
|
||||
}
|
||||
// 设备状态:param 是状态值(必填);设备属性:param 是比较值(必填)
|
||||
if (
|
||||
(isDeviceStatus || isDeviceProperty) &&
|
||||
(condition.param === undefined ||
|
||||
condition.param === null ||
|
||||
condition.param === '')
|
||||
) {
|
||||
callback(
|
||||
new Error(
|
||||
`${prefix}:${isDeviceStatus ? '设备状态' : '比较值'}不能为空`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
// 当前时间:TODAY 不需要 param;BETWEEN_TIME 需要双段「v1,v2」;其它需要单段
|
||||
if (isCurrentTime) {
|
||||
const op = condition.operator;
|
||||
if (op === IotRuleSceneTriggerTimeOperatorEnum.TODAY.value) {
|
||||
// TODAY 无需 param
|
||||
} else if (
|
||||
op === IotRuleSceneTriggerTimeOperatorEnum.BETWEEN_TIME.value
|
||||
) {
|
||||
const parts = condition.param
|
||||
? String(condition.param).split(',')
|
||||
: [];
|
||||
if (parts.length < 2 || !parts[0] || !parts[1]) {
|
||||
callback(new Error(`${prefix}:起止时间不能为空`));
|
||||
return;
|
||||
}
|
||||
} else if (!condition.param) {
|
||||
callback(new Error(`${prefix}:时间值不能为空`));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
callback();
|
||||
}
|
||||
|
|
@ -183,7 +269,10 @@ function validateActions(_rule: any, value: any, callback: any) {
|
|||
callback(new Error(`执行器 ${i + 1}:产品不能为空`));
|
||||
return;
|
||||
}
|
||||
if (!action.deviceId) {
|
||||
// deviceId = 0 表示「全部设备」(DEVICE_SELECTOR_OPTIONS.ALL_DEVICES);
|
||||
// 后端 IotDevicePropertySetSceneRuleAction / IotDeviceServiceInvokeSceneRuleAction
|
||||
// 均支持广播执行,因此 0 是合法值,仅 undefined / null 视为未选
|
||||
if (action.deviceId === undefined || action.deviceId === null) {
|
||||
callback(new Error(`执行器 ${i + 1}:设备不能为空`));
|
||||
return;
|
||||
}
|
||||
|
|
@ -199,9 +288,9 @@ function validateActions(_rule: any, value: any, callback: any) {
|
|||
return;
|
||||
}
|
||||
}
|
||||
// 仅恢复告警动作需要选择已有告警配置;触发告警动作不需要预选 alertConfigId
|
||||
if (
|
||||
(action.type === IotRuleSceneActionTypeEnum.ALERT_TRIGGER ||
|
||||
action.type === IotRuleSceneActionTypeEnum.ALERT_RECOVER) &&
|
||||
action.type === IotRuleSceneActionTypeEnum.ALERT_RECOVER &&
|
||||
!action.alertConfigId
|
||||
) {
|
||||
callback(new Error(`执行器 ${i + 1}:告警配置不能为空`));
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ const formattedDataSpecsList = computed(() => {
|
|||
return '';
|
||||
}
|
||||
return props.data.property.dataSpecsList
|
||||
.map((item) => `${item.value}-${item.name}`)
|
||||
.map((item) => `${item.name}-${item.value}`)
|
||||
.join('、');
|
||||
});
|
||||
|
||||
|
|
@ -44,8 +44,8 @@ const shortText = computed(() => {
|
|||
}
|
||||
const first = list[0];
|
||||
return list.length > 1
|
||||
? `${first.value}-${first.name} 等 ${list.length} 项`
|
||||
: `${first.value}-${first.name}`;
|
||||
? `${first.name}-${first.value} 等 ${list.length} 项`
|
||||
: `${first.name}-${first.value}`;
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -68,5 +68,6 @@ function handleChange(val: any) {
|
|||
<ThingModelStructDataSpecs
|
||||
v-if="dataSpecs.childDataType === IoTDataSpecsDataTypeEnum.STRUCT"
|
||||
v-model="dataSpecs.dataSpecsList"
|
||||
field-path="property.dataSpecs.dataSpecsList"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
<script lang="ts" setup>
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import { IoTDataSpecsDataTypeEnum } from '@vben/constants';
|
||||
import { isEmpty } from '@vben/utils';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
|
|
@ -17,7 +18,11 @@ const validateEnumName = buildIdentifierLikeNameValidator('枚举描述');
|
|||
|
||||
/** 添加枚举项 */
|
||||
function addEnum() {
|
||||
dataSpecsList.value.push({ name: '', value: '' } as any);
|
||||
dataSpecsList.value.push({
|
||||
dataType: IoTDataSpecsDataTypeEnum.ENUM,
|
||||
name: '',
|
||||
value: '',
|
||||
} as any);
|
||||
}
|
||||
|
||||
/** 删除枚举项 */
|
||||
|
|
|
|||
|
|
@ -27,24 +27,99 @@ function unitChange(unitSpecs: any) {
|
|||
dataSpecs.value.unitName = unitName;
|
||||
dataSpecs.value.unit = unit;
|
||||
}
|
||||
|
||||
/** 校验最小值 */
|
||||
function validateMin(_rule: any, _value: any, callback: any) {
|
||||
const min = Number(dataSpecs.value.min);
|
||||
const max = Number(dataSpecs.value.max);
|
||||
if (Number.isNaN(min)) {
|
||||
callback(new Error('请输入有效的数值'));
|
||||
return;
|
||||
}
|
||||
if (!Number.isNaN(max) && min >= max) {
|
||||
callback(new Error('最小值必须小于最大值'));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
}
|
||||
|
||||
/** 校验最大值 */
|
||||
function validateMax(_rule: any, _value: any, callback: any) {
|
||||
const min = Number(dataSpecs.value.min);
|
||||
const max = Number(dataSpecs.value.max);
|
||||
if (Number.isNaN(max)) {
|
||||
callback(new Error('请输入有效的数值'));
|
||||
return;
|
||||
}
|
||||
if (!Number.isNaN(min) && max <= min) {
|
||||
callback(new Error('最大值必须大于最小值'));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
}
|
||||
|
||||
/** 校验步长 */
|
||||
function validateStep(_rule: any, _value: any, callback: any) {
|
||||
const step = Number(dataSpecs.value.step);
|
||||
if (Number.isNaN(step)) {
|
||||
callback(new Error('请输入有效的数值'));
|
||||
return;
|
||||
}
|
||||
if (step <= 0) {
|
||||
callback(new Error('步长必须大于 0'));
|
||||
return;
|
||||
}
|
||||
const min = Number(dataSpecs.value.min);
|
||||
const max = Number(dataSpecs.value.max);
|
||||
if (!Number.isNaN(min) && !Number.isNaN(max) && step > max - min) {
|
||||
callback(new Error('步长不能大于最大值与最小值的差值'));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElFormItem label="取值范围">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1">
|
||||
<ElFormItem
|
||||
:rules="[
|
||||
{ required: true, message: '最小值不能为空' },
|
||||
{ validator: validateMin, trigger: 'blur' },
|
||||
]"
|
||||
class="mb-0 flex-1"
|
||||
prop="property.dataSpecs.min"
|
||||
>
|
||||
<ElInput v-model="dataSpecs.min" placeholder="请输入最小值" />
|
||||
</div>
|
||||
</ElFormItem>
|
||||
<span class="mx-2">~</span>
|
||||
<div class="flex-1">
|
||||
<ElFormItem
|
||||
:rules="[
|
||||
{ required: true, message: '最大值不能为空' },
|
||||
{ validator: validateMax, trigger: 'blur' },
|
||||
]"
|
||||
class="mb-0 flex-1"
|
||||
prop="property.dataSpecs.max"
|
||||
>
|
||||
<ElInput v-model="dataSpecs.max" placeholder="请输入最大值" />
|
||||
</div>
|
||||
</ElFormItem>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="步长">
|
||||
<ElFormItem
|
||||
:rules="[
|
||||
{ required: true, message: '步长不能为空' },
|
||||
{ validator: validateStep, trigger: 'blur' },
|
||||
]"
|
||||
label="步长"
|
||||
prop="property.dataSpecs.step"
|
||||
>
|
||||
<ElInput v-model="dataSpecs.step" placeholder="请输入步长" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="单位">
|
||||
<ElFormItem
|
||||
:rules="[{ required: true, message: '请选择单位' }]"
|
||||
label="单位"
|
||||
prop="property.dataSpecs.unit"
|
||||
>
|
||||
<ElSelect
|
||||
:model-value="
|
||||
dataSpecs.unit ? `${dataSpecs.unitName}-${dataSpecs.unit}` : ''
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { onMounted, ref } from 'vue';
|
|||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { IoTDataSpecsDataTypeEnum } from '@vben/constants';
|
||||
import { isEmpty } from '@vben/utils';
|
||||
import { cloneDeep, isEmpty } from '@vben/utils';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import {
|
||||
|
|
@ -20,13 +20,33 @@ import { ThingModelFormRules } from '#/api/iot/thingmodel';
|
|||
|
||||
import ThingModelProperty from '../property.vue';
|
||||
|
||||
const props = defineProps<{ modelValue: any }>();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
/** 父表单中 dataSpecsList 的 prop 路径(点号分隔),默认 property.dataSpecsList;
|
||||
* array 嵌套 struct 时父级需传 'property.dataSpecs.dataSpecsList',
|
||||
* 否则父表单 validate() 无法定位该字段,非空校验不会触发。 */
|
||||
fieldPath?: string;
|
||||
modelValue: any;
|
||||
}>(),
|
||||
{
|
||||
fieldPath: 'property.dataSpecsList',
|
||||
},
|
||||
);
|
||||
const emits = defineEmits(['update:modelValue']);
|
||||
const dataSpecsList = useVModel(props, 'modelValue', emits) as Ref<any[]>;
|
||||
|
||||
const structFormRef = ref();
|
||||
const formData = ref<any>(buildEmptyFormData());
|
||||
|
||||
/** 校验结构体属性对象非空 */
|
||||
function validateStructSpecsList(_rule: any, _value: any, callback: any) {
|
||||
if (isEmpty(dataSpecsList.value)) {
|
||||
callback(new Error('请至少添加一个结构体属性对象'));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
}
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
try {
|
||||
|
|
@ -70,14 +90,15 @@ const [Modal, modalApi] = useVbenModal({
|
|||
if (isEmpty(data)) {
|
||||
return;
|
||||
}
|
||||
// 编辑回显时 cloneDeep,避免弹窗 v-model 改到原始对象(用户取消时不污染外层 dataSpecsList)
|
||||
formData.value = {
|
||||
identifier: data.identifier ?? '',
|
||||
name: data.name ?? '',
|
||||
description: data.description ?? '',
|
||||
property: {
|
||||
dataType: data.childDataType ?? IoTDataSpecsDataTypeEnum.INT,
|
||||
dataSpecs: data.dataSpecs ?? {},
|
||||
dataSpecsList: data.dataSpecsList ?? [],
|
||||
dataSpecs: data.dataSpecs ? cloneDeep(data.dataSpecs) : {},
|
||||
dataSpecsList: data.dataSpecsList ? cloneDeep(data.dataSpecsList) : [],
|
||||
},
|
||||
};
|
||||
},
|
||||
|
|
@ -115,7 +136,11 @@ onMounted(() => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<ElFormItem label="属性对象">
|
||||
<ElFormItem
|
||||
:prop="fieldPath"
|
||||
:rules="[{ validator: validateStructSpecsList, trigger: 'change' }]"
|
||||
label="属性对象"
|
||||
>
|
||||
<div
|
||||
v-for="(item, index) in dataSpecsList"
|
||||
:key="index"
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { ref } from 'vue';
|
|||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { IoTDataSpecsDataTypeEnum } from '@vben/constants';
|
||||
import { isEmpty } from '@vben/utils';
|
||||
import { cloneDeep, isEmpty } from '@vben/utils';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import {
|
||||
|
|
@ -78,15 +78,15 @@ const [Modal, modalApi] = useVbenModal({
|
|||
if (isEmpty(data)) {
|
||||
return;
|
||||
}
|
||||
// 设置到 values
|
||||
// 编辑回显时 cloneDeep,避免弹窗 v-model 改到原始对象(用户取消时不污染外层 thingModelParams)
|
||||
formData.value = {
|
||||
identifier: data.identifier ?? '',
|
||||
name: data.name ?? '',
|
||||
description: data.description ?? '',
|
||||
property: {
|
||||
dataType: data.dataType ?? IoTDataSpecsDataTypeEnum.INT,
|
||||
dataSpecs: data.dataSpecs ?? {},
|
||||
dataSpecsList: data.dataSpecsList ?? [],
|
||||
dataSpecs: data.dataSpecs ? cloneDeep(data.dataSpecs) : {},
|
||||
dataSpecsList: data.dataSpecsList ? cloneDeep(data.dataSpecsList) : [],
|
||||
},
|
||||
};
|
||||
},
|
||||
|
|
|
|||
|
|
@ -129,11 +129,9 @@ export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
|
|||
onChange: async () => {
|
||||
await formApi?.setFieldValue('areaId', undefined);
|
||||
},
|
||||
options: list.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
})),
|
||||
options: list,
|
||||
placeholder: '请选择库区',
|
||||
props: { label: 'name', value: 'id' },
|
||||
};
|
||||
},
|
||||
},
|
||||
|
|
@ -151,11 +149,9 @@ export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
|
|||
: [];
|
||||
return {
|
||||
clearable: true,
|
||||
options: list.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
})),
|
||||
options: list,
|
||||
placeholder: '请选择库位',
|
||||
props: { label: 'name', value: 'id' },
|
||||
};
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,3 +1,13 @@
|
|||
/** MES 单据状态常量 */
|
||||
export const MesOrderStatusConstants = {
|
||||
DRAFT: 0,
|
||||
CONFIRMED: 1,
|
||||
APPROVING: 2,
|
||||
APPROVED: 3,
|
||||
FINISHED: 4,
|
||||
CANCELLED: 5,
|
||||
} as const;
|
||||
|
||||
/** MES 物料/产品标识枚举 */
|
||||
export const MesItemOrProductEnum = {
|
||||
ITEM: {
|
||||
|
|
@ -10,14 +20,121 @@ export const MesItemOrProductEnum = {
|
|||
},
|
||||
} as const;
|
||||
|
||||
/** MES 工具状态枚举 */
|
||||
export const MesToolStatusEnum = {
|
||||
STORE: 1,
|
||||
ISSUE: 2,
|
||||
REPAIR: 3,
|
||||
SCRAP: 4,
|
||||
} as const;
|
||||
|
||||
/** MES 保养维护类型枚举 */
|
||||
export const MesMaintenTypeEnum = {
|
||||
REGULAR: 1,
|
||||
USAGE: 2,
|
||||
} as const;
|
||||
|
||||
/** MES 设备状态枚举 */
|
||||
export const MesDvMachineryStatusEnum = {
|
||||
STOP: 1,
|
||||
PRODUCING: 2,
|
||||
MAINTENANCE: 3,
|
||||
} as const;
|
||||
|
||||
/** MES 假期类型枚举 */
|
||||
export const HolidayType = {
|
||||
WORKDAY: 1,
|
||||
HOLIDAY: 2,
|
||||
} as const;
|
||||
|
||||
/** MES 排班计划状态枚举 */
|
||||
export const MesCalPlanStatusEnum = {
|
||||
PREPARE: 0,
|
||||
CONFIRMED: 1,
|
||||
} as const;
|
||||
|
||||
/** MES 轮班方式枚举 */
|
||||
export const MesCalShiftTypeEnum = {
|
||||
SINGLE: 1,
|
||||
TWO: 2,
|
||||
THREE: 3,
|
||||
} as const;
|
||||
|
||||
/** MES 倒班方式枚举 */
|
||||
export const MesCalShiftMethodEnum = {
|
||||
QUARTER: 1,
|
||||
MONTH: 2,
|
||||
WEEK: 3,
|
||||
DAY: 4,
|
||||
} as const;
|
||||
|
||||
/** MES 点检保养项目类型枚举 */
|
||||
export const MesDvSubjectTypeEnum = {
|
||||
CHECK: 1,
|
||||
MAINTENANCE: 2,
|
||||
} as const;
|
||||
|
||||
/** MES 点检保养方案状态枚举 */
|
||||
export const MesDvCheckPlanStatusEnum = {
|
||||
PREPARE: 0,
|
||||
ENABLED: 1,
|
||||
} as const;
|
||||
|
||||
/** MES 设备保养记录状态枚举 */
|
||||
export const MesDvMaintenRecordStatusEnum = {
|
||||
PREPARE: MesOrderStatusConstants.DRAFT,
|
||||
SUBMITTED: MesOrderStatusConstants.FINISHED,
|
||||
} as const;
|
||||
|
||||
/** MES 设备保养明细结果枚举 */
|
||||
export const MesDvMaintenStatusEnum = {
|
||||
NORMAL: 1,
|
||||
ABNORMAL: 2,
|
||||
} as const;
|
||||
|
||||
/** MES 设备点检记录状态枚举 */
|
||||
export const MesDvCheckRecordStatusEnum = {
|
||||
DRAFT: 10,
|
||||
FINISHED: 20,
|
||||
} as const;
|
||||
|
||||
/** MES 设备点检结果枚举 */
|
||||
export const MesDvCheckResultEnum = {
|
||||
NORMAL: 1,
|
||||
ABNORMAL: 2,
|
||||
} as const;
|
||||
|
||||
/** MES 维修工单状态枚举 */
|
||||
export const MesDvRepairStatusEnum = {
|
||||
PREPARE: MesOrderStatusConstants.DRAFT,
|
||||
CONFIRMED: MesOrderStatusConstants.CONFIRMED,
|
||||
APPROVING: MesOrderStatusConstants.APPROVING,
|
||||
FINISHED: MesOrderStatusConstants.FINISHED,
|
||||
} as const;
|
||||
|
||||
/** MES 维修结果枚举 */
|
||||
export const MesDvRepairResultEnum = {
|
||||
PASS: 1,
|
||||
FAIL: 2,
|
||||
} as const;
|
||||
|
||||
/** MES 自动编码规则 Code 枚举 */
|
||||
export const MesAutoCodeRuleCode = {
|
||||
CAL_PLAN_CODE: 'CAL_PLAN_CODE',
|
||||
CAL_TEAM_CODE: 'CAL_TEAM_CODE',
|
||||
DV_CHECK_PLAN_CODE: 'DV_CHECK_PLAN_CODE',
|
||||
DV_MACHINERY_TYPE_CODE: 'DV_MACHINERY_TYPE_CODE',
|
||||
DV_MACHINERY_CODE: 'DV_MACHINERY_CODE',
|
||||
DV_REPAIR_CODE: 'DV_REPAIR_CODE',
|
||||
DV_SUBJECT_CODE: 'DV_SUBJECT_CODE',
|
||||
MD_CLIENT_CODE: 'MD_CLIENT_CODE',
|
||||
MD_ITEM_TYPE_CODE: 'MD_ITEM_TYPE_CODE',
|
||||
MD_ITEM_CODE: 'MD_ITEM_CODE',
|
||||
MD_VENDOR_CODE: 'MD_VENDOR_CODE',
|
||||
MD_WORKSTATION_CODE: 'MD_WORKSTATION_CODE',
|
||||
MD_WORKSHOP_CODE: 'MD_WORKSHOP_CODE',
|
||||
TM_TOOL_TYPE_CODE: 'TM_TOOL_TYPE_CODE',
|
||||
TM_TOOL_CODE: 'TM_TOOL_CODE',
|
||||
} as const;
|
||||
|
||||
/** MES 编码规则分段类型枚举 */
|
||||
|
|
|
|||
|
|
@ -183,6 +183,23 @@ const MES_DICT = {
|
|||
MES_MD_AUTO_CODE_PART_TYPE: 'mes_md_auto_code_part_type', // MES 编码规则分段类型
|
||||
MES_CLIENT_TYPE: 'mes_client_type', // MES 客户类型
|
||||
MES_VENDOR_LEVEL: 'mes_vendor_level', // MES 供应商级别
|
||||
MES_CAL_HOLIDAY_TYPE: 'mes_cal_holiday_type', // MES 假期类型
|
||||
MES_CAL_SHIFT_TYPE: 'mes_cal_shift_type', // MES 轮班方式
|
||||
MES_CAL_SHIFT_METHOD: 'mes_cal_shift_method', // MES 倒班方式
|
||||
MES_CAL_CALENDAR_TYPE: 'mes_cal_calendar_type', // MES 班组类型
|
||||
MES_CAL_PLAN_STATUS: 'mes_cal_plan_status', // MES 排班计划状态
|
||||
MES_TM_TOOL_STATUS: 'mes_tm_tool_status', // MES 工具状态
|
||||
MES_TM_MAINTEN_TYPE: 'mes_tm_mainten_type', // MES 保养维护类型
|
||||
MES_DV_MACHINERY_STATUS: 'mes_dv_machinery_status', // MES 设备状态
|
||||
MES_DV_SUBJECT_TYPE: 'mes_dv_subject_type', // MES 点检保养项目类型
|
||||
MES_DV_CYCLE_TYPE: 'mes_dv_cycle_type', // MES 点检保养周期类型
|
||||
MES_DV_CHECK_PLAN_STATUS: 'mes_dv_check_plan_status', // MES 点检保养方案状态
|
||||
MES_MAINTEN_RECORD_STATUS: 'mes_mainten_record_status', // MES 保养记录状态
|
||||
MES_MAINTEN_STATUS: 'mes_mainten_status', // MES 保养结果
|
||||
MES_DV_REPAIR_STATUS: 'mes_dv_repair_status', // MES 维修工单状态
|
||||
MES_DV_REPAIR_RESULT: 'mes_dv_repair_result', // MES 维修结果
|
||||
MES_DV_CHECK_RECORD_STATUS: 'mes_dv_check_record_status', // MES 点检记录状态
|
||||
MES_DV_CHECK_RESULT: 'mes_dv_check_result', // MES 点检结果
|
||||
MES_WM_BARCODE_BIZ_TYPE: 'mes_wm_barcode_biz_type', // MES 条码业务类型
|
||||
MES_WM_BARCODE_FORMAT: 'mes_wm_barcode_format', // MES 条码格式
|
||||
MES_WM_PRODUCT_SALES_STATUS: 'mes_wm_product_sales_status', // MES 销售出库单状态
|
||||
|
|
|
|||
Loading…
Reference in New Issue