fix(iot): 修复 IoT 复评后续对齐问题
- 补齐设备详情子设备、Modbus 操作权限 - 修复属性搜索清空、模拟器空参数、告警处理备注校验 - 修复 HTTP 数据目的 URL 回显、Redis Stream 密码必填 - 优化固件上传读取时机,补充 isEmptyVal 并复用 JSON 参数校验 - 修正场景产品状态字典和 antd ValueInput 图标导入pull/348/head
parent
ab697925cf
commit
d2763dc044
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ const textareaProps = computed(() => {
|
||||||
const fileUploadProps = computed(() => {
|
const fileUploadProps = computed(() => {
|
||||||
return {
|
return {
|
||||||
...props.fileUploadProps,
|
...props.fileUploadProps,
|
||||||
|
returnText: true,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -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[];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
},
|
},
|
||||||
]"
|
]"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ const textareaProps = computed(() => {
|
||||||
const fileUploadProps = computed(() => {
|
const fileUploadProps = computed(() => {
|
||||||
return {
|
return {
|
||||||
...props.fileUploadProps,
|
...props.fileUploadProps,
|
||||||
|
returnText: true,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -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[];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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: '正在处理...' });
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
},
|
},
|
||||||
]"
|
]"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue