fix(iot): 修复 IoT 复评后续对齐问题

- 补齐设备详情子设备、Modbus 操作权限
- 修复属性搜索清空、模拟器空参数、告警处理备注校验
- 修复 HTTP 数据目的 URL 回显、Redis Stream 密码必填
- 优化固件上传读取时机,补充 isEmptyVal 并复用 JSON 参数校验
- 修正场景产品状态字典和 antd ValueInput 图标导入
pull/348/head
YunaiV 2026-05-25 00:43:50 +08:00
parent ab697925cf
commit d2763dc044
27 changed files with 178 additions and 98 deletions

View File

@ -32,6 +32,7 @@ const props = withDefaults(defineProps<FileUploadProps>(), {
multiple: false, multiple: false,
api: undefined, api: undefined,
resultField: '', resultField: '',
returnText: false,
showDescription: false, showDescription: false,
}); });
const emit = defineEmits([ const emit = defineEmits([
@ -147,9 +148,6 @@ function handleUploadError(error: any) {
* @returns 是否允许上传 * @returns 是否允许上传
*/ */
async function beforeUpload(file: File) { async function beforeUpload(file: File) {
const fileContent = await file.text();
emit('returnText', fileContent);
// //
if (fileList.value!.length >= props.maxNumber) { if (fileList.value!.length >= props.maxNumber) {
message.error($t('ui.upload.maxNumber', [props.maxNumber])); message.error($t('ui.upload.maxNumber', [props.maxNumber]));
@ -176,6 +174,10 @@ async function beforeUpload(file: File) {
// //
uploadNumber.value++; uploadNumber.value++;
if (props.returnText) {
const fileContent = await file.text();
emit('returnText', fileContent);
}
return true; return true;
} }

View File

@ -58,6 +58,7 @@ const textareaProps = computed(() => {
const fileUploadProps = computed(() => { const fileUploadProps = computed(() => {
return { return {
...props.fileUploadProps, ...props.fileUploadProps,
returnText: true,
}; };
}); });
</script> </script>

View File

@ -27,6 +27,7 @@ export interface FileUploadProps {
maxSize?: number; // 文件最大多少MB maxSize?: number; // 文件最大多少MB
multiple?: boolean; // 是否支持多选 multiple?: boolean; // 是否支持多选
resultField?: string; // support xxx.xxx.xx resultField?: string; // support xxx.xxx.xx
returnText?: boolean; // 是否返回文件文本内容
showDescription?: boolean; // 是否显示下面的描述 showDescription?: boolean; // 是否显示下面的描述
value?: string | string[]; value?: string | string[];
} }

View File

@ -45,10 +45,6 @@ function handleProcess(row: AlertRecordApi.AlertRecord) {
}), }),
]), ]),
async onOk() { async onOk() {
if (!processRemark.value) {
message.warning('请输入处理原因');
throw new Error('请输入处理原因');
}
const hideLoading = message.loading({ const hideLoading = message.loading({
content: '正在处理...', content: '正在处理...',
duration: 0, duration: 0,

View File

@ -14,7 +14,7 @@ import { computed, h, onMounted, ref } from 'vue';
import { confirm, useVbenModal } from '@vben/common-ui'; import { confirm, useVbenModal } from '@vben/common-ui';
import { DICT_TYPE, ModbusFunctionCodeOptions } from '@vben/constants'; import { DICT_TYPE, ModbusFunctionCodeOptions } from '@vben/constants';
import { Button, message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getModbusConfig } from '#/api/iot/device/modbus/config'; import { getModbusConfig } from '#/api/iot/device/modbus/config';
@ -307,7 +307,16 @@ onMounted(async () => {
<!-- 连接配置区域 --> <!-- 连接配置区域 -->
<ConfigDescriptions :data="modbusConfig" class="mb-4"> <ConfigDescriptions :data="modbusConfig" class="mb-4">
<template #extra> <template #extra>
<Button type="primary" @click="handleEditConfig"></Button> <TableAction
:actions="[
{
label: '编辑',
type: 'primary',
auth: ['iot:device:create'],
onClick: handleEditConfig,
},
]"
/>
</template> </template>
</ConfigDescriptions> </ConfigDescriptions>
@ -320,6 +329,7 @@ onMounted(async () => {
label: '新增点位', label: '新增点位',
type: 'primary', type: 'primary',
icon: ACTION_ICON.ADD, icon: ACTION_ICON.ADD,
auth: ['iot:device:create'],
onClick: handleAddPoint, onClick: handleAddPoint,
}, },
]" ]"
@ -331,12 +341,14 @@ onMounted(async () => {
{ {
label: '编辑', label: '编辑',
type: 'link', type: 'link',
auth: ['iot:device:update'],
onClick: () => handleEditPoint(row), onClick: () => handleEditPoint(row),
}, },
{ {
label: '删除', label: '删除',
type: 'link', type: 'link',
danger: true, danger: true,
auth: ['iot:device:delete'],
popConfirm: { popConfirm: {
title: `确定要删除点位【${row.name}】吗?`, title: `确定要删除点位【${row.name}】吗?`,
confirm: () => handleDeletePoint(row), confirm: () => handleDeletePoint(row),

View File

@ -312,13 +312,15 @@ async function handleEventPost(row: ThingModelApi.ThingModel) {
const valueStr = formData.value[row.identifier!]; const valueStr = formData.value[row.identifier!];
let eventValue: any; let eventValue: any;
if (valueStr) { if (valueStr === undefined || valueStr === null || valueStr === '') {
try { message.warning('请输入事件参数');
eventValue = JSON.parse(valueStr); return;
} catch { }
message.error('事件参数格式错误请输入有效的JSON格式'); try {
return; eventValue = JSON.parse(valueStr);
} } catch {
message.error('事件参数格式错误请输入有效的JSON格式');
return;
} }
// IotDeviceEventPostReqDTO { identifier, value, time } // IotDeviceEventPostReqDTO { identifier, value, time }
@ -399,21 +401,23 @@ async function handleServiceInvoke(row: ThingModelApi.ThingModel) {
const valueStr = formData.value[row.identifier!]; const valueStr = formData.value[row.identifier!];
let inputParams: any = {}; let inputParams: any = {};
if (valueStr) { if (valueStr === undefined || valueStr === null || valueStr === '') {
try { message.warning('请输入服务参数');
inputParams = JSON.parse(valueStr); return;
} catch { }
message.error('服务参数格式错误请输入有效的JSON格式'); try {
return; inputParams = JSON.parse(valueStr);
} } catch {
if ( message.error('服务参数格式错误请输入有效的JSON格式');
typeof inputParams !== 'object' || return;
inputParams === null || }
Array.isArray(inputParams) if (
) { typeof inputParams !== 'object' ||
message.error('服务参数必须是 JSON 对象'); inputParams === null ||
return; Array.isArray(inputParams)
} ) {
message.error('服务参数必须是 JSON 对象');
return;
} }
// IotDeviceServiceInvokeReqDTO { identifier, inputParams } // IotDeviceServiceInvokeReqDTO { identifier, inputParams }

View File

@ -317,6 +317,7 @@ watch(
label: '添加子设备', label: '添加子设备',
type: 'primary', type: 'primary',
icon: ACTION_ICON.ADD, icon: ACTION_ICON.ADD,
auth: ['iot:device:update'],
onClick: openAddModal, onClick: openAddModal,
}, },
{ {
@ -324,6 +325,7 @@ watch(
type: 'primary', type: 'primary',
danger: true, danger: true,
icon: ACTION_ICON.DELETE, icon: ACTION_ICON.DELETE,
auth: ['iot:device:update'],
disabled: isEmpty(checkedIds), disabled: isEmpty(checkedIds),
onClick: handleUnbindBatch, onClick: handleUnbindBatch,
}, },
@ -342,6 +344,7 @@ watch(
label: '解绑', label: '解绑',
type: 'link', type: 'link',
danger: true, danger: true,
auth: ['iot:device:update'],
onClick: () => handleUnbind(row), onClick: () => handleUnbind(row),
}, },
]" ]"

View File

@ -207,6 +207,13 @@ function handleQuery() {
} }
} }
/** 搜索关键词变化 */
function handleKeywordChange(event: Event) {
if (!(event.target as HTMLInputElement).value) {
handleQuery();
}
}
/** 视图切换 */ /** 视图切换 */
async function handleViewModeChange(mode: 'card' | 'list') { async function handleViewModeChange(mode: 'card' | 'list') {
if (viewMode.value === mode) { if (viewMode.value === mode) {
@ -281,6 +288,7 @@ onBeforeUnmount(() => {
allow-clear allow-clear
placeholder="请输入属性名称、标识符" placeholder="请输入属性名称、标识符"
style="width: 240px" style="width: 240px"
@change="handleKeywordChange"
@press-enter="handleQuery" @press-enter="handleQuery"
/> />
<Switch <Switch

View File

@ -13,7 +13,11 @@ import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteOtaFirmware, getOtaFirmwarePage } from '#/api/iot/ota/firmware'; import { deleteOtaFirmware, getOtaFirmwarePage } from '#/api/iot/ota/firmware';
import { $t } from '#/locales'; import { $t } from '#/locales';
import { getProductName, useGridColumns, useGridFormSchema } from './data'; import {
getProductName,
useGridColumns,
useGridFormSchema,
} from './data';
import OtaFirmwareForm from './modules/form.vue'; import OtaFirmwareForm from './modules/form.vue';
const { push } = useRouter(); const { push } = useRouter();
@ -116,7 +120,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
<!-- 所属产品列点击跳产品详情 --> <!-- 所属产品列点击跳产品详情 -->
<template #productName="{ row }"> <template #productName="{ row }">
<a <a
v-if="row.productId" v-if="row.productId && getProductName(row.productId) !== '-'"
class="cursor-pointer text-primary hover:underline" class="cursor-pointer text-primary hover:underline"
@click="handleOpenProductDetail(row.productId)" @click="handleOpenProductDetail(row.productId)"
> >

View File

@ -21,21 +21,31 @@ const fullUrl = computed(() =>
urlPath.value ? urlPrefix.value + urlPath.value : '', urlPath.value ? urlPrefix.value + urlPath.value : '',
); );
function syncUrlFields(url?: string) {
if (url?.startsWith('https://')) {
urlPrefix.value = 'https://';
urlPath.value = url.slice(8);
} else if (url?.startsWith('http://')) {
urlPrefix.value = 'http://';
urlPath.value = url.slice(7);
} else {
urlPath.value = url ?? '';
}
}
watch([urlPrefix, urlPath], () => { watch([urlPrefix, urlPath], () => {
config.value.url = fullUrl.value; config.value.url = fullUrl.value;
}); });
watch(
() => config.value?.url,
(url) => syncUrlFields(url),
{ immediate: true },
);
onMounted(() => { onMounted(() => {
if (!isEmpty(config.value)) { if (!isEmpty(config.value)) {
if (config.value.url?.startsWith('https://')) { syncUrlFields(config.value.url);
urlPrefix.value = 'https://';
urlPath.value = config.value.url.slice(8);
} else if (config.value.url?.startsWith('http://')) {
urlPrefix.value = 'http://';
urlPath.value = config.value.url.slice(7);
} else {
urlPath.value = config.value.url ?? '';
}
return; return;
} }
config.value = { config.value = {

View File

@ -76,7 +76,11 @@ onMounted(() => {
class="w-full" class="w-full"
/> />
</Form.Item> </Form.Item>
<Form.Item :name="['config', 'password']" label="密码"> <Form.Item
:name="['config', 'password']"
:rules="[{ required: true, message: '密码不能为空', trigger: 'blur' }]"
label="密码"
>
<Input.Password v-model:value="config.password" placeholder="请输入密码" /> <Input.Password v-model:value="config.password" placeholder="请输入密码" />
</Form.Item> </Form.Item>
<Form.Item <Form.Item

View File

@ -12,6 +12,7 @@ import {
JsonParamsInputTypeEnum, JsonParamsInputTypeEnum,
} from '@vben/constants'; } from '@vben/constants';
import { IconifyIcon } from '@vben/icons'; import { IconifyIcon } from '@vben/icons';
import { isEmptyVal } from '@vben/utils';
import { useVModel } from '@vueuse/core'; import { useVModel } from '@vueuse/core';
import { Button, Input, Popover, Tag } from 'ant-design-vue'; import { Button, Input, Popover, Tag } from 'ant-design-vue';
@ -242,10 +243,8 @@ function handleParamsChange() {
// //
for (const param of paramsList.value) { for (const param of paramsList.value) {
if ( const value = parsed[param.identifier];
param.required && if (param.required && isEmptyVal(value)) {
(!parsed[param.identifier] || parsed[param.identifier] === '')
) {
jsonError.value = JSON_PARAMS_INPUT_CONSTANTS.PARAM_REQUIRED_ERROR( jsonError.value = JSON_PARAMS_INPUT_CONSTANTS.PARAM_REQUIRED_ERROR(
param.name, param.name,
); );

View File

@ -6,6 +6,7 @@ import {
IoTDataSpecsDataTypeEnum, IoTDataSpecsDataTypeEnum,
IotRuleSceneTriggerConditionParameterOperatorEnum, IotRuleSceneTriggerConditionParameterOperatorEnum,
} from '@vben/constants'; } from '@vben/constants';
import { IconifyIcon } from '@vben/icons';
import { useVModel } from '@vueuse/core'; import { useVModel } from '@vueuse/core';
import { import {

View File

@ -79,7 +79,7 @@ onMounted(() => {
{{ product.productKey }} {{ product.productKey }}
</div> </div>
</div> </div>
<DictTag :type="DICT_TYPE.COMMON_STATUS" :value="product.status" /> <DictTag :type="DICT_TYPE.IOT_PRODUCT_STATUS" :value="product.status" />
</div> </div>
</Select.Option> </Select.Option>
</Select> </Select>

View File

@ -35,6 +35,7 @@ const props = withDefaults(defineProps<FileUploadProps>(), {
multiple: false, multiple: false,
api: undefined, api: undefined,
resultField: '', resultField: '',
returnText: false,
showDescription: false, showDescription: false,
}); });
const emit = defineEmits([ const emit = defineEmits([
@ -163,9 +164,6 @@ function handleUploadError(error: any) {
*/ */
/* eslint-disable unicorn/no-nested-ternary */ /* eslint-disable unicorn/no-nested-ternary */
async function beforeUpload(file: File) { async function beforeUpload(file: File) {
const fileContent = await file.text();
emit('returnText', fileContent);
// 使 getValue // 使 getValue
const currentFiles = getValue(); const currentFiles = getValue();
const currentCount = Array.isArray(currentFiles) const currentCount = Array.isArray(currentFiles)
@ -198,6 +196,10 @@ async function beforeUpload(file: File) {
// //
uploadNumber.value++; uploadNumber.value++;
if (props.returnText) {
const fileContent = await file.text();
emit('returnText', fileContent);
}
return true; return true;
} }

View File

@ -58,6 +58,7 @@ const textareaProps = computed(() => {
const fileUploadProps = computed(() => { const fileUploadProps = computed(() => {
return { return {
...props.fileUploadProps, ...props.fileUploadProps,
returnText: true,
}; };
}); });
</script> </script>

View File

@ -27,6 +27,7 @@ export interface FileUploadProps {
maxSize?: number; // 文件最大多少 MB maxSize?: number; // 文件最大多少 MB
multiple?: boolean; // 是否支持多选 multiple?: boolean; // 是否支持多选
resultField?: string; // support xxx.xxx.xx resultField?: string; // support xxx.xxx.xx
returnText?: boolean; // 是否返回文件文本内容
showDescription?: boolean; // 是否显示下面的描述 showDescription?: boolean; // 是否显示下面的描述
value?: string | string[]; value?: string | string[];
} }

View File

@ -44,12 +44,6 @@ async function handleProcess(row: AlertRecordApi.AlertRecord) {
cancelButtonText: '取消', cancelButtonText: '取消',
inputType: 'textarea', inputType: 'textarea',
inputPlaceholder: '请输入处理原因', inputPlaceholder: '请输入处理原因',
inputValidator: (value: string) => {
if (!value) {
return '请输入处理原因';
}
return true;
},
}, },
); );
const loadingInstance = ElLoading.service({ text: '正在处理...' }); const loadingInstance = ElLoading.service({ text: '正在处理...' });

View File

@ -14,7 +14,7 @@ import { computed, h, onMounted, ref } from 'vue';
import { confirm, useVbenModal } from '@vben/common-ui'; import { confirm, useVbenModal } from '@vben/common-ui';
import { DICT_TYPE, ModbusFunctionCodeOptions } from '@vben/constants'; import { DICT_TYPE, ModbusFunctionCodeOptions } from '@vben/constants';
import { ElButton, ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getModbusConfig } from '#/api/iot/device/modbus/config'; import { getModbusConfig } from '#/api/iot/device/modbus/config';
@ -307,7 +307,16 @@ onMounted(async () => {
<!-- 连接配置区域 --> <!-- 连接配置区域 -->
<ConfigDescriptions :data="modbusConfig" class="mb-4"> <ConfigDescriptions :data="modbusConfig" class="mb-4">
<template #extra> <template #extra>
<ElButton type="primary" @click="handleEditConfig"></ElButton> <TableAction
:actions="[
{
label: '编辑',
type: 'primary',
auth: ['iot:device:create'],
onClick: handleEditConfig,
},
]"
/>
</template> </template>
</ConfigDescriptions> </ConfigDescriptions>
@ -320,6 +329,7 @@ onMounted(async () => {
label: '新增点位', label: '新增点位',
type: 'primary', type: 'primary',
icon: ACTION_ICON.ADD, icon: ACTION_ICON.ADD,
auth: ['iot:device:create'],
onClick: handleAddPoint, onClick: handleAddPoint,
}, },
]" ]"
@ -332,12 +342,14 @@ onMounted(async () => {
label: '编辑', label: '编辑',
type: 'primary', type: 'primary',
link: true, link: true,
auth: ['iot:device:update'],
onClick: () => handleEditPoint(row), onClick: () => handleEditPoint(row),
}, },
{ {
label: '删除', label: '删除',
type: 'danger', type: 'danger',
link: true, link: true,
auth: ['iot:device:delete'],
popConfirm: { popConfirm: {
title: `确定要删除点位【${row.name}】吗?`, title: `确定要删除点位【${row.name}】吗?`,
confirm: () => handleDeletePoint(row), confirm: () => handleDeletePoint(row),

View File

@ -202,13 +202,15 @@ async function handleEventPost(row: ThingModelApi.ThingModel) {
const valueStr = formData.value[row.identifier!]; const valueStr = formData.value[row.identifier!];
let eventValue: any; let eventValue: any;
if (valueStr) { if (valueStr === undefined || valueStr === null || valueStr === '') {
try { ElMessage.warning('请输入事件参数');
eventValue = JSON.parse(valueStr); return;
} catch { }
ElMessage.error('事件参数格式错误请输入有效的JSON格式'); try {
return; eventValue = JSON.parse(valueStr);
} } catch {
ElMessage.error('事件参数格式错误请输入有效的JSON格式');
return;
} }
// IotDeviceEventPostReqDTO { identifier, value, time } // IotDeviceEventPostReqDTO { identifier, value, time }
@ -286,21 +288,23 @@ async function handleServiceInvoke(row: ThingModelApi.ThingModel) {
const valueStr = formData.value[row.identifier!]; const valueStr = formData.value[row.identifier!];
let inputParams: any = {}; let inputParams: any = {};
if (valueStr) { if (valueStr === undefined || valueStr === null || valueStr === '') {
try { ElMessage.warning('请输入服务参数');
inputParams = JSON.parse(valueStr); return;
} catch { }
ElMessage.error('服务参数格式错误请输入有效的JSON格式'); try {
return; inputParams = JSON.parse(valueStr);
} } catch {
if ( ElMessage.error('服务参数格式错误请输入有效的JSON格式');
typeof inputParams !== 'object' || return;
inputParams === null || }
Array.isArray(inputParams) if (
) { typeof inputParams !== 'object' ||
ElMessage.error('服务参数必须是 JSON 对象'); inputParams === null ||
return; Array.isArray(inputParams)
} ) {
ElMessage.error('服务参数必须是 JSON 对象');
return;
} }
// IotDeviceServiceInvokeReqDTO { identifier, inputParams } // IotDeviceServiceInvokeReqDTO { identifier, inputParams }

View File

@ -315,12 +315,14 @@ watch(
label: '添加子设备', label: '添加子设备',
type: 'primary', type: 'primary',
icon: ACTION_ICON.ADD, icon: ACTION_ICON.ADD,
auth: ['iot:device:update'],
onClick: openAddModal, onClick: openAddModal,
}, },
{ {
label: '批量解绑', label: '批量解绑',
type: 'danger', type: 'danger',
icon: ACTION_ICON.DELETE, icon: ACTION_ICON.DELETE,
auth: ['iot:device:update'],
disabled: isEmpty(checkedIds), disabled: isEmpty(checkedIds),
onClick: handleUnbindBatch, onClick: handleUnbindBatch,
}, },
@ -340,6 +342,7 @@ watch(
label: '解绑', label: '解绑',
type: 'danger', type: 'danger',
link: true, link: true,
auth: ['iot:device:update'],
onClick: () => handleUnbind(row), onClick: () => handleUnbind(row),
}, },
]" ]"

View File

@ -282,6 +282,7 @@ onBeforeUnmount(() => {
clearable clearable
placeholder="请输入属性名称、标识符" placeholder="请输入属性名称、标识符"
style="width: 240px" style="width: 240px"
@clear="handleQuery"
@keyup.enter="handleQuery" @keyup.enter="handleQuery"
/> />
<ElSwitch <ElSwitch

View File

@ -13,7 +13,11 @@ import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteOtaFirmware, getOtaFirmwarePage } from '#/api/iot/ota/firmware'; import { deleteOtaFirmware, getOtaFirmwarePage } from '#/api/iot/ota/firmware';
import { $t } from '#/locales'; import { $t } from '#/locales';
import { getProductName, useGridColumns, useGridFormSchema } from './data'; import {
getProductName,
useGridColumns,
useGridFormSchema,
} from './data';
import OtaFirmwareForm from './modules/form.vue'; import OtaFirmwareForm from './modules/form.vue';
const { push } = useRouter(); const { push } = useRouter();
@ -113,7 +117,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
<!-- 所属产品列点击跳产品详情 --> <!-- 所属产品列点击跳产品详情 -->
<template #productName="{ row }"> <template #productName="{ row }">
<a <a
v-if="row.productId" v-if="row.productId && getProductName(row.productId) !== '-'"
class="cursor-pointer text-[var(--el-color-primary)] hover:underline" class="cursor-pointer text-[var(--el-color-primary)] hover:underline"
@click="handleOpenProductDetail(row.productId)" @click="handleOpenProductDetail(row.productId)"
> >

View File

@ -21,21 +21,31 @@ const fullUrl = computed(() =>
urlPath.value ? urlPrefix.value + urlPath.value : '', urlPath.value ? urlPrefix.value + urlPath.value : '',
); );
function syncUrlFields(url?: string) {
if (url?.startsWith('https://')) {
urlPrefix.value = 'https://';
urlPath.value = url.slice(8);
} else if (url?.startsWith('http://')) {
urlPrefix.value = 'http://';
urlPath.value = url.slice(7);
} else {
urlPath.value = url ?? '';
}
}
watch([urlPrefix, urlPath], () => { watch([urlPrefix, urlPath], () => {
config.value.url = fullUrl.value; config.value.url = fullUrl.value;
}); });
watch(
() => config.value?.url,
(url) => syncUrlFields(url),
{ immediate: true },
);
onMounted(() => { onMounted(() => {
if (!isEmpty(config.value)) { if (!isEmpty(config.value)) {
if (config.value.url?.startsWith('https://')) { syncUrlFields(config.value.url);
urlPrefix.value = 'https://';
urlPath.value = config.value.url.slice(8);
} else if (config.value.url?.startsWith('http://')) {
urlPrefix.value = 'http://';
urlPath.value = config.value.url.slice(7);
} else {
urlPath.value = config.value.url ?? '';
}
return; return;
} }
config.value = { config.value = {

View File

@ -82,7 +82,11 @@ onMounted(() => {
class="w-full" class="w-full"
/> />
</ElFormItem> </ElFormItem>
<ElFormItem prop="config.password" label="密码"> <ElFormItem
prop="config.password"
:rules="[{ required: true, message: '密码不能为空', trigger: 'blur' }]"
label="密码"
>
<ElInput <ElInput
v-model="config.password" v-model="config.password"
type="password" type="password"

View File

@ -12,6 +12,7 @@ import {
JsonParamsInputTypeEnum, JsonParamsInputTypeEnum,
} from '@vben/constants'; } from '@vben/constants';
import { IconifyIcon } from '@vben/icons'; import { IconifyIcon } from '@vben/icons';
import { isEmptyVal } from '@vben/utils';
import { useVModel } from '@vueuse/core'; import { useVModel } from '@vueuse/core';
import { ElButton, ElInput, ElPopover, ElTag } from 'element-plus'; import { ElButton, ElInput, ElPopover, ElTag } from 'element-plus';
@ -242,10 +243,8 @@ function handleParamsChange() {
// //
for (const param of paramsList.value) { for (const param of paramsList.value) {
if ( const value = parsed[param.identifier];
param.required && if (param.required && isEmptyVal(value)) {
(!parsed[param.identifier] || parsed[param.identifier] === '')
) {
jsonError.value = JSON_PARAMS_INPUT_CONSTANTS.PARAM_REQUIRED_ERROR( jsonError.value = JSON_PARAMS_INPUT_CONSTANTS.PARAM_REQUIRED_ERROR(
param.name, param.name,
); );

View File

@ -80,7 +80,7 @@ onMounted(() => {
{{ product.productKey }} {{ product.productKey }}
</div> </div>
</div> </div>
<DictTag :type="DICT_TYPE.COMMON_STATUS" :value="product.status" /> <DictTag :type="DICT_TYPE.IOT_PRODUCT_STATUS" :value="product.status" />
</div> </div>
</ElOption> </ElOption>
</ElSelect> </ElSelect>