fix(iot): 修复 13 处 bug 并完成 codex 三轮收口
按 codex 两轮 review 分批处理 IoT 模块 13 处 bug,对第二轮反馈中
B42/B47 的类型/字段问题做最终收尾,所有修复 web-antd / web-ele 两端同步。
主要修复:
- B91 设备分组:删除前校验 deviceCount,分组下有设备时弹警告
- B40 物模型 array 数据规格:从 Radio.Group @change 事件正确取值(antd)
- B42 物模型属性历史:list 写入时按 idx 生成 _rowKey,模板 row-key="_rowKey"
list 类型改 IotDeviceApi.DeviceProperty & { _rowKey: string }
匹配后端 IotDevicePropertyMapper.xml 实际返回的字段
(修掉 codex 指出的 antd row-key TS2322 与 ele 同毫秒撞键)
- B119 物模型表单:edit 模式禁用 identifier 编辑
- B47 场景规则主条件:产品/设备切换时清 deviceId/identifier/operator/value
(修掉 codex 指出的 condition.value.param TS2339,Trigger 无 param)
- B44 数据目的数据库配置:SQL 复制按钮 setTimeout 在 onBeforeUnmount 中清理
- B51 场景规则首页统计:total 取接口 result.total,其余基于当前页
- B29 产品卡片视图:图标为 URL 时改用 <img> 渲染,复用 @vben/utils 的 isHttpUrl
- B43 首页设备地图:移除过度设计的 AbortController,回归 vue3 源项目同款
InfoWindow 监听写法,querySelector 限定到 .BMap_bubble_content 子树
- B105 场景规则设备选择器:productId 变化后旧 deviceId 不在新列表则清空
- B45 通用 key-value 编辑器:v-for key 改用递增的 _uid,避免编辑/删除时 DOM 复用错乱
- B132 设备导入表单:beforeUpload 校验 .xls/.xlsx
- B126 设备详情:四个 tab 子组件 v-if 增加 device.id 守卫
附带工具收敛:
- @vben-core/shared/utils 新增 formatDayjs,统一 antd TimePicker/DatePicker
value-format 后回传的 Dayjs|string 归一
- 场景规则首页 updateStatistics 补回 JSDoc,对齐文件内其他 function 风格
验证:
- 改动文件 pnpm exec eslint 0 error
- pnpm -F @vben/web-antd / @vben/web-ele exec vue-tsc --noEmit --skipLibCheck
过滤 src/views/iot/|src/api/iot/ 均 0 hit
pull/348/head
parent
241cf76788
commit
aeff25209d
|
|
@ -60,6 +60,7 @@
|
||||||
"pinia": "catalog:",
|
"pinia": "catalog:",
|
||||||
"steady-xml": "catalog:",
|
"steady-xml": "catalog:",
|
||||||
"tinymce": "catalog:",
|
"tinymce": "catalog:",
|
||||||
|
"tyme4ts": "catalog:",
|
||||||
"video.js": "catalog:",
|
"video.js": "catalog:",
|
||||||
"vue": "catalog:",
|
"vue": "catalog:",
|
||||||
"vue-dompurify-html": "catalog:",
|
"vue-dompurify-html": "catalog:",
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,9 @@ export namespace MesDvCheckPlanMachineryApi {
|
||||||
|
|
||||||
/** 查询指定方案的设备列表 */
|
/** 查询指定方案的设备列表 */
|
||||||
export function getCheckPlanMachineryListByPlan(planId: number) {
|
export function getCheckPlanMachineryListByPlan(planId: number) {
|
||||||
return requestClient.get<MesDvCheckPlanMachineryApi.CheckPlanMachinery[]>(`/mes/dv/check-plan-machinery/list-by-plan?planId=${planId}`);
|
return requestClient.get<MesDvCheckPlanMachineryApi.CheckPlanMachinery[]>(
|
||||||
|
`/mes/dv/check-plan-machinery/list-by-plan?planId=${planId}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 新增方案设备关联 */
|
/** 新增方案设备关联 */
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,5 @@ export function deleteAutoCodeRule(id: number) {
|
||||||
|
|
||||||
/** 导出编码规则 */
|
/** 导出编码规则 */
|
||||||
export function exportAutoCodeRule(params: PageParam) {
|
export function exportAutoCodeRule(params: PageParam) {
|
||||||
return requestClient.download('/mes/md/auto-code-rule/export-excel', {
|
return requestClient.download('/mes/md/auto-code-rule/export-excel', { params });
|
||||||
params,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,9 +34,7 @@ export namespace MesMdItemApi {
|
||||||
|
|
||||||
/** 查询物料产品分页 */
|
/** 查询物料产品分页 */
|
||||||
export function getItemPage(params: PageParam) {
|
export function getItemPage(params: PageParam) {
|
||||||
return requestClient.get<PageResult<MesMdItemApi.Item>>('/mes/md/item/page', {
|
return requestClient.get<PageResult<MesMdItemApi.Item>>('/mes/md/item/page', { params });
|
||||||
params,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 查询物料产品详情 */
|
/** 查询物料产品详情 */
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,7 @@ onMounted(async () => {
|
||||||
<Tabs v-model:active-key="activeTab" class="mt-4">
|
<Tabs v-model:active-key="activeTab" class="mt-4">
|
||||||
<Tabs.TabPane key="info" tab="设备信息">
|
<Tabs.TabPane key="info" tab="设备信息">
|
||||||
<DeviceDetailsInfo
|
<DeviceDetailsInfo
|
||||||
v-if="activeTab === 'info'"
|
v-if="activeTab === 'info' && device.id"
|
||||||
:device="device"
|
:device="device"
|
||||||
:product="product"
|
:product="product"
|
||||||
/>
|
/>
|
||||||
|
|
@ -127,7 +127,7 @@ onMounted(async () => {
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
<Tabs.TabPane key="simulator" tab="模拟设备">
|
<Tabs.TabPane key="simulator" tab="模拟设备">
|
||||||
<DeviceDetailsSimulator
|
<DeviceDetailsSimulator
|
||||||
v-if="activeTab === 'simulator'"
|
v-if="activeTab === 'simulator' && device.id"
|
||||||
:device="device"
|
:device="device"
|
||||||
:product="product"
|
:product="product"
|
||||||
:thing-model-list="thingModelList"
|
:thing-model-list="thingModelList"
|
||||||
|
|
@ -135,7 +135,7 @@ onMounted(async () => {
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
<Tabs.TabPane key="config" tab="设备配置">
|
<Tabs.TabPane key="config" tab="设备配置">
|
||||||
<DeviceDetailConfig
|
<DeviceDetailConfig
|
||||||
v-if="activeTab === 'config'"
|
v-if="activeTab === 'config' && device.id"
|
||||||
:device="device"
|
:device="device"
|
||||||
@success="() => getDeviceData(id)"
|
@success="() => getDeviceData(id)"
|
||||||
/>
|
/>
|
||||||
|
|
@ -151,7 +151,7 @@ onMounted(async () => {
|
||||||
tab="Modbus 配置"
|
tab="Modbus 配置"
|
||||||
>
|
>
|
||||||
<DeviceModbusConfig
|
<DeviceModbusConfig
|
||||||
v-if="activeTab === 'modbus'"
|
v-if="activeTab === 'modbus' && device.id"
|
||||||
:device="device"
|
:device="device"
|
||||||
:product="product"
|
:product="product"
|
||||||
:thing-model-list="thingModelList"
|
:thing-model-list="thingModelList"
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ defineProps<{ deviceId: number }>();
|
||||||
const dialogVisible = ref(false); // 弹窗的是否展示
|
const dialogVisible = ref(false); // 弹窗的是否展示
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const viewMode = ref<'chart' | 'list'>('chart'); // 视图模式状态
|
const viewMode = ref<'chart' | 'list'>('chart'); // 视图模式状态
|
||||||
const list = ref<IotDeviceApi.DevicePropertyDetail[]>([]); // 列表的数据
|
const list = ref<Array<IotDeviceApi.DeviceProperty & { _rowKey: string }>>([]); // 列表的数据
|
||||||
const total = ref(0); // 总数据量
|
const total = ref(0); // 总数据量
|
||||||
const thingModelDataType = ref<string>(''); // 物模型数据类型
|
const thingModelDataType = ref<string>(''); // 物模型数据类型
|
||||||
const propertyIdentifier = ref<string>(''); // 属性标识符
|
const propertyIdentifier = ref<string>(''); // 属性标识符
|
||||||
|
|
@ -151,9 +151,11 @@ const paginationConfig = computed(() => ({
|
||||||
async function getList() {
|
async function getList() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
// 后端直接返回数组
|
|
||||||
const data = await getHistoryDevicePropertyList(queryParams);
|
const data = await getHistoryDevicePropertyList(queryParams);
|
||||||
list.value = (data || []) as IotDeviceApi.DevicePropertyDetail[];
|
list.value = (data || []).map((item, idx) => ({
|
||||||
|
...item,
|
||||||
|
_rowKey: `${item.updateTime ?? ''}-${idx}`, // 后端直接返回数组,仅含 value/updateTime,给每行补 _rowKey 保证唯一
|
||||||
|
}));
|
||||||
total.value = list.value.length;
|
total.value = list.value.length;
|
||||||
|
|
||||||
// 如果是图表模式且支持图表展示,等待渲染图表
|
// 如果是图表模式且支持图表展示,等待渲染图表
|
||||||
|
|
@ -438,7 +440,7 @@ defineExpose({ open }); // 提供 open 方法,用于打开弹窗
|
||||||
:data-source="list"
|
:data-source="list"
|
||||||
:pagination="paginationConfig"
|
:pagination="paginationConfig"
|
||||||
:scroll="{ y: 500 }"
|
:scroll="{ y: 500 }"
|
||||||
row-key="updateTime"
|
row-key="_rowKey"
|
||||||
size="small"
|
size="small"
|
||||||
>
|
>
|
||||||
<template #bodyCell="{ column, record }">
|
<template #bodyCell="{ column, record }">
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,11 @@ const [Modal, modalApi] = useVbenModal({
|
||||||
|
|
||||||
/** 上传前 */
|
/** 上传前 */
|
||||||
function beforeUpload(file: FileType) {
|
function beforeUpload(file: FileType) {
|
||||||
|
const fileName = file.name?.toLowerCase() ?? '';
|
||||||
|
if (!fileName.endsWith('.xls') && !fileName.endsWith('.xlsx')) {
|
||||||
|
message.error('只能上传 Excel 文件(.xls / .xlsx)');
|
||||||
|
return Upload.LIST_IGNORE;
|
||||||
|
}
|
||||||
formApi.setFieldValue('file', file);
|
formApi.setFieldValue('file', file);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,10 @@ function handleEdit(row: IotDeviceGroupApi.DeviceGroup) {
|
||||||
|
|
||||||
/** 删除设备分组 */
|
/** 删除设备分组 */
|
||||||
async function handleDelete(row: IotDeviceGroupApi.DeviceGroup) {
|
async function handleDelete(row: IotDeviceGroupApi.DeviceGroup) {
|
||||||
|
if (row.deviceCount && row.deviceCount > 0) {
|
||||||
|
message.warning(`分组「${row.name}」下存在 ${row.deviceCount} 台设备,无法删除`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const hideLoading = message.loading({
|
const hideLoading = message.loading({
|
||||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||||
duration: 0,
|
duration: 0,
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { onMounted, ref } from 'vue';
|
||||||
import { useAccess } from '@vben/access';
|
import { useAccess } from '@vben/access';
|
||||||
import { DICT_TYPE, ProductStatusEnum } from '@vben/constants';
|
import { DICT_TYPE, ProductStatusEnum } from '@vben/constants';
|
||||||
import { IconifyIcon } from '@vben/icons';
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
import { isHttpUrl } from '@vben/utils';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
|
@ -117,7 +118,14 @@ onMounted(() => {
|
||||||
<div
|
<div
|
||||||
class="flex size-9 shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-[#40a9ff] to-[#1890ff] text-white"
|
class="flex size-9 shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-[#40a9ff] to-[#1890ff] text-white"
|
||||||
>
|
>
|
||||||
|
<img
|
||||||
|
v-if="isHttpUrl(item.icon)"
|
||||||
|
:src="item.icon"
|
||||||
|
alt=""
|
||||||
|
class="size-6 object-contain"
|
||||||
|
/>
|
||||||
<IconifyIcon
|
<IconifyIcon
|
||||||
|
v-else
|
||||||
:icon="item.icon || 'lucide:box'"
|
:icon="item.icon || 'lucide:box'"
|
||||||
class="text-xl"
|
class="text-xl"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -16,15 +16,18 @@ const props = defineProps<{
|
||||||
const emit = defineEmits(['update:modelValue']);
|
const emit = defineEmits(['update:modelValue']);
|
||||||
|
|
||||||
interface KeyValueItem {
|
interface KeyValueItem {
|
||||||
|
_uid: number;
|
||||||
key: string;
|
key: string;
|
||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let uidCounter = 0;
|
||||||
const items = ref<KeyValueItem[]>([]); // 内部 key-value 项列表
|
const items = ref<KeyValueItem[]>([]); // 内部 key-value 项列表
|
||||||
|
|
||||||
/** 添加项目 */
|
/** 添加项目 */
|
||||||
function addItem() {
|
function addItem() {
|
||||||
items.value.push({ key: '', value: '' });
|
uidCounter += 1;
|
||||||
|
items.value.push({ _uid: uidCounter, key: '', value: '' });
|
||||||
updateModelValue();
|
updateModelValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,16 +57,16 @@ watch(
|
||||||
if (isEmpty(val) || !isEmpty(items.value)) {
|
if (isEmpty(val) || !isEmpty(items.value)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
items.value = Object.entries(props.modelValue).map(([key, value]) => ({
|
items.value = Object.entries(props.modelValue).map(([key, value]) => {
|
||||||
key,
|
uidCounter += 1;
|
||||||
value,
|
return { _uid: uidCounter, key, value };
|
||||||
}));
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-for="(item, index) in items" :key="index" class="mb-2 flex w-full">
|
<div v-for="(item, index) in items" :key="item._uid" class="mb-2 flex w-full">
|
||||||
<Input v-model:value="item.key" class="mr-2" placeholder="键" />
|
<Input v-model:value="item.key" class="mr-2" placeholder="键" />
|
||||||
<Input v-model:value="item.value" placeholder="值" />
|
<Input v-model:value="item.value" placeholder="值" />
|
||||||
<Button class="ml-2" type="link" danger @click="removeItem(index)">
|
<Button class="ml-2" type="link" danger @click="removeItem(index)">
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, ref } from 'vue';
|
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
import { IconifyIcon } from '@vben/icons';
|
import { IconifyIcon } from '@vben/icons';
|
||||||
import { isEmpty } from '@vben/utils';
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
@ -30,14 +30,28 @@ const config = useVModel(props, 'modelValue', emit);
|
||||||
const showSqlTip = ref(false);
|
const showSqlTip = ref(false);
|
||||||
const copied = ref(false);
|
const copied = ref(false);
|
||||||
const { copy } = useClipboard();
|
const { copy } = useClipboard();
|
||||||
|
let copyResetTimer: null | ReturnType<typeof setTimeout> = null;
|
||||||
|
|
||||||
async function handleCopySql() {
|
async function handleCopySql() {
|
||||||
await copy(TABLE_SQL);
|
await copy(TABLE_SQL);
|
||||||
copied.value = true;
|
copied.value = true;
|
||||||
message.success('建表 SQL 已复制到剪贴板');
|
message.success('建表 SQL 已复制到剪贴板');
|
||||||
setTimeout(() => (copied.value = false), 2000);
|
if (copyResetTimer) {
|
||||||
|
clearTimeout(copyResetTimer);
|
||||||
|
}
|
||||||
|
copyResetTimer = setTimeout(() => {
|
||||||
|
copied.value = false;
|
||||||
|
copyResetTimer = null;
|
||||||
|
}, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (copyResetTimer) {
|
||||||
|
clearTimeout(copyResetTimer);
|
||||||
|
copyResetTimer = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!isEmpty(config.value)) {
|
if (!isEmpty(config.value)) {
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -124,15 +124,24 @@ function handleTriggerTypeChange(type: number) {
|
||||||
|
|
||||||
/** 处理产品变化事件 */
|
/** 处理产品变化事件 */
|
||||||
function handleProductChange() {
|
function handleProductChange() {
|
||||||
// 产品变化时清空设备和属性
|
const trigger = condition.value;
|
||||||
condition.value.deviceId = undefined;
|
trigger.deviceId = undefined;
|
||||||
condition.value.identifier = '';
|
trigger.identifier = '';
|
||||||
|
trigger.operator = undefined;
|
||||||
|
// 主条件比较值字段是 Trigger.value(不是 TriggerCondition.param)
|
||||||
|
trigger.value = '';
|
||||||
|
propertyType.value = '';
|
||||||
|
propertyConfig.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 处理设备变化事件 */
|
/** 处理设备变化事件 */
|
||||||
function handleDeviceChange() {
|
function handleDeviceChange() {
|
||||||
// 设备变化时清空属性
|
const trigger = condition.value;
|
||||||
condition.value.identifier = '';
|
trigger.identifier = '';
|
||||||
|
trigger.operator = undefined;
|
||||||
|
trigger.value = '';
|
||||||
|
propertyType.value = '';
|
||||||
|
propertyConfig.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -55,16 +55,26 @@ async function getDeviceList() {
|
||||||
// 监听产品变化
|
// 监听产品变化
|
||||||
watch(
|
watch(
|
||||||
() => props.productId,
|
() => props.productId,
|
||||||
(newProductId) => {
|
async (newProductId, oldProductId) => {
|
||||||
if (newProductId) {
|
if (!newProductId) {
|
||||||
getDeviceList();
|
|
||||||
} else {
|
|
||||||
deviceList.value = [];
|
deviceList.value = [];
|
||||||
// 清空当前选择的设备
|
if (props.modelValue !== undefined && props.modelValue !== null) {
|
||||||
if (props.modelValue) {
|
|
||||||
emit('update:modelValue', undefined);
|
emit('update:modelValue', undefined);
|
||||||
emit('change', undefined);
|
emit('change', undefined);
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await getDeviceList();
|
||||||
|
// 切换到新 productId 时,旧 deviceId 不在新列表里则清空
|
||||||
|
if (
|
||||||
|
oldProductId !== undefined &&
|
||||||
|
oldProductId !== newProductId &&
|
||||||
|
props.modelValue !== undefined &&
|
||||||
|
props.modelValue !== null &&
|
||||||
|
!deviceList.value.some((d: any) => d.id === props.modelValue)
|
||||||
|
) {
|
||||||
|
emit('update:modelValue', undefined);
|
||||||
|
emit('change', undefined);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
|
|
|
||||||
|
|
@ -182,10 +182,10 @@ function getNextExecutionTime(row: RuleSceneApi.SceneRule): Date | null {
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 基于当前页列表刷新统计数据 */
|
/** 刷新规则统计卡片数据 */
|
||||||
function updateStatistics(rows: RuleSceneApi.SceneRule[]) {
|
function updateStatistics(rows: RuleSceneApi.SceneRule[], total?: number) {
|
||||||
statistics.value = {
|
statistics.value = {
|
||||||
total: rows.length,
|
total: total ?? rows.length,
|
||||||
enabled: rows.filter((item) => item.status === CommonStatusEnum.ENABLE)
|
enabled: rows.filter((item) => item.status === CommonStatusEnum.ENABLE)
|
||||||
.length,
|
.length,
|
||||||
disabled: rows.filter((item) => item.status === CommonStatusEnum.DISABLE)
|
disabled: rows.filter((item) => item.status === CommonStatusEnum.DISABLE)
|
||||||
|
|
@ -210,7 +210,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
pageSize: page.pageSize,
|
pageSize: page.pageSize,
|
||||||
...formValues,
|
...formValues,
|
||||||
});
|
});
|
||||||
updateStatistics(result.list || []);
|
updateStatistics(result.list || [], result.total);
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,8 @@ const childDataTypeOptions = getDataTypeOptions().filter(
|
||||||
const dataSpecs = useVModel(props, 'modelValue', emits) as Ref<any>;
|
const dataSpecs = useVModel(props, 'modelValue', emits) as Ref<any>;
|
||||||
|
|
||||||
/** 元素类型切到 struct 时,初始化 dataSpecsList 占位 */
|
/** 元素类型切到 struct 时,初始化 dataSpecsList 占位 */
|
||||||
function handleChange(val: any) {
|
function handleChange(e: any) {
|
||||||
|
const val = e?.target?.value ?? e;
|
||||||
if (val !== IoTDataSpecsDataTypeEnum.STRUCT) {
|
if (val !== IoTDataSpecsDataTypeEnum.STRUCT) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -231,7 +231,11 @@ function removeDataSpecs(val: any) {
|
||||||
label="标识符"
|
label="标识符"
|
||||||
name="identifier"
|
name="identifier"
|
||||||
>
|
>
|
||||||
<Input v-model:value="formData.identifier" placeholder="请输入标识符" />
|
<Input
|
||||||
|
v-model:value="formData.identifier"
|
||||||
|
:disabled="formData.id != null"
|
||||||
|
placeholder="请输入标识符"
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<!-- 属性配置 -->
|
<!-- 属性配置 -->
|
||||||
<ThingModelProperty
|
<ThingModelProperty
|
||||||
|
|
|
||||||
|
|
@ -47,13 +47,33 @@ async function waitTableReady(): Promise<void> {
|
||||||
if (preSelectedIds.value.length === 0) {
|
if (preSelectedIds.value.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!multiple.value) {
|
for (let index = 0; index < MAX_TABLE_READY_FRAMES; index += 1) {
|
||||||
const selected = checked && row ? [row] : [];
|
if (queryFinished.value) {
|
||||||
selectedRows.value = selected;
|
const rows = getTableRows();
|
||||||
await syncSingleSelection(selected[0]);
|
if (latestQueryRows.value.length === 0 && rows.length === 0) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
if (latestQueryRows.value.length > 0 && rows.length > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await waitNextFrame();
|
||||||
}
|
}
|
||||||
selectedRows.value = records;
|
}
|
||||||
|
|
||||||
|
/** 获取多选记录,包含 VXE reserve 跨页记录 */
|
||||||
|
function getMultipleSelectedRows() {
|
||||||
|
const selectedMap = new Map<number, MesMdClientApi.Client>();
|
||||||
|
const records = [
|
||||||
|
...(gridApi.grid.getCheckboxReserveRecords?.() ?? []),
|
||||||
|
...(gridApi.grid.getCheckboxRecords?.() ?? []),
|
||||||
|
] as MesMdClientApi.Client[];
|
||||||
|
records.forEach((row) => {
|
||||||
|
if (row.id != null) {
|
||||||
|
selectedMap.set(row.id, row);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return [...selectedMap.values()];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 处理勾选变化 */
|
/** 处理勾选变化 */
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@
|
||||||
"pinia": "catalog:",
|
"pinia": "catalog:",
|
||||||
"steady-xml": "catalog:",
|
"steady-xml": "catalog:",
|
||||||
"tinymce": "catalog:",
|
"tinymce": "catalog:",
|
||||||
|
"tyme4ts": "catalog:",
|
||||||
"video.js": "catalog:",
|
"video.js": "catalog:",
|
||||||
"vue": "catalog:",
|
"vue": "catalog:",
|
||||||
"vue-dompurify-html": "catalog:",
|
"vue-dompurify-html": "catalog:",
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,9 @@ export namespace MesDvCheckPlanMachineryApi {
|
||||||
|
|
||||||
/** 查询指定方案的设备列表 */
|
/** 查询指定方案的设备列表 */
|
||||||
export function getCheckPlanMachineryListByPlan(planId: number) {
|
export function getCheckPlanMachineryListByPlan(planId: number) {
|
||||||
return requestClient.get<MesDvCheckPlanMachineryApi.CheckPlanMachinery[]>(`/mes/dv/check-plan-machinery/list-by-plan?planId=${planId}`);
|
return requestClient.get<MesDvCheckPlanMachineryApi.CheckPlanMachinery[]>(
|
||||||
|
`/mes/dv/check-plan-machinery/list-by-plan?planId=${planId}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 新增方案设备关联 */
|
/** 新增方案设备关联 */
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,9 @@ export namespace MesDvCheckPlanSubjectApi {
|
||||||
|
|
||||||
/** 查询指定方案的项目列表 */
|
/** 查询指定方案的项目列表 */
|
||||||
export function getCheckPlanSubjectListByPlan(planId: number) {
|
export function getCheckPlanSubjectListByPlan(planId: number) {
|
||||||
return requestClient.get<MesDvCheckPlanSubjectApi.CheckPlanSubject[]>(`/mes/dv/check-plan-subject/list-by-plan?planId=${planId}`);
|
return requestClient.get<MesDvCheckPlanSubjectApi.CheckPlanSubject[]>(
|
||||||
|
`/mes/dv/check-plan-subject/list-by-plan?planId=${planId}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 新增方案项目关联 */
|
/** 新增方案项目关联 */
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,5 @@ export function deleteAutoCodeRule(id: number) {
|
||||||
|
|
||||||
/** 导出编码规则 */
|
/** 导出编码规则 */
|
||||||
export function exportAutoCodeRule(params: PageParam) {
|
export function exportAutoCodeRule(params: PageParam) {
|
||||||
return requestClient.download('/mes/md/auto-code-rule/export-excel', {
|
return requestClient.download('/mes/md/auto-code-rule/export-excel', { params });
|
||||||
params,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,9 +34,7 @@ export namespace MesMdItemApi {
|
||||||
|
|
||||||
/** 查询物料产品分页 */
|
/** 查询物料产品分页 */
|
||||||
export function getItemPage(params: PageParam) {
|
export function getItemPage(params: PageParam) {
|
||||||
return requestClient.get<PageResult<MesMdItemApi.Item>>('/mes/md/item/page', {
|
return requestClient.get<PageResult<MesMdItemApi.Item>>('/mes/md/item/page', { params });
|
||||||
params,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 查询物料产品详情 */
|
/** 查询物料产品详情 */
|
||||||
|
|
|
||||||
|
|
@ -18,22 +18,35 @@ export namespace MesTmToolTypeApi {
|
||||||
|
|
||||||
/** 查询工具类型分页 */
|
/** 查询工具类型分页 */
|
||||||
export function getToolTypePage(params: PageParam) {
|
export function getToolTypePage(params: PageParam) {
|
||||||
return requestClient.get<PageResult<MesTmToolTypeApi.ToolType>>(
|
return requestClient.get<PageResult<MesTmToolTypeApi.ToolType>>('/mes/tm/tool-type/page', { params });
|
||||||
'/mes/tm/tool-type/page',
|
|
||||||
{ params },
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 查询工具类型精简列表 */
|
/** 查询工具类型精简列表 */
|
||||||
export function getToolTypeSimpleList() {
|
export function getToolTypeSimpleList() {
|
||||||
return requestClient.get<MesTmToolTypeApi.ToolType[]>(
|
return requestClient.get<MesTmToolTypeApi.ToolType[]>('/mes/tm/tool-type/simple-list');
|
||||||
'/mes/tm/tool-type/simple-list',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 查询工具类型详情 */
|
/** 查询工具类型详情 */
|
||||||
export function getToolType(id: number) {
|
export function getToolType(id: number) {
|
||||||
return requestClient.get<MesTmToolTypeApi.ToolType>(
|
return requestClient.get<MesTmToolTypeApi.ToolType>(`/mes/tm/tool-type/get?id=${id}`);
|
||||||
`/mes/tm/tool-type/get?id=${id}`,
|
}
|
||||||
);
|
|
||||||
|
/** 新增工具类型 */
|
||||||
|
export function createToolType(data: MesTmToolTypeApi.ToolType) {
|
||||||
|
return requestClient.post('/mes/tm/tool-type/create', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 修改工具类型 */
|
||||||
|
export function updateToolType(data: MesTmToolTypeApi.ToolType) {
|
||||||
|
return requestClient.put('/mes/tm/tool-type/update', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除工具类型 */
|
||||||
|
export function deleteToolType(id: number) {
|
||||||
|
return requestClient.delete(`/mes/tm/tool-type/delete?id=${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 导出工具类型 */
|
||||||
|
export function exportToolType(params: any) {
|
||||||
|
return requestClient.download('/mes/tm/tool-type/export-excel', { params });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,7 @@ onMounted(async () => {
|
||||||
<ElTabs v-model="activeTab" class="mt-4">
|
<ElTabs v-model="activeTab" class="mt-4">
|
||||||
<ElTabPane name="info" label="设备信息">
|
<ElTabPane name="info" label="设备信息">
|
||||||
<DeviceDetailsInfo
|
<DeviceDetailsInfo
|
||||||
v-if="activeTab === 'info'"
|
v-if="activeTab === 'info' && device.id"
|
||||||
:device="device"
|
:device="device"
|
||||||
:product="product"
|
:product="product"
|
||||||
/>
|
/>
|
||||||
|
|
@ -127,7 +127,7 @@ onMounted(async () => {
|
||||||
</ElTabPane>
|
</ElTabPane>
|
||||||
<ElTabPane name="simulator" label="模拟设备">
|
<ElTabPane name="simulator" label="模拟设备">
|
||||||
<DeviceDetailsSimulator
|
<DeviceDetailsSimulator
|
||||||
v-if="activeTab === 'simulator'"
|
v-if="activeTab === 'simulator' && device.id"
|
||||||
:device="device"
|
:device="device"
|
||||||
:product="product"
|
:product="product"
|
||||||
:thing-model-list="thingModelList"
|
:thing-model-list="thingModelList"
|
||||||
|
|
@ -135,7 +135,7 @@ onMounted(async () => {
|
||||||
</ElTabPane>
|
</ElTabPane>
|
||||||
<ElTabPane name="config" label="设备配置">
|
<ElTabPane name="config" label="设备配置">
|
||||||
<DeviceDetailConfig
|
<DeviceDetailConfig
|
||||||
v-if="activeTab === 'config'"
|
v-if="activeTab === 'config' && device.id"
|
||||||
:device="device"
|
:device="device"
|
||||||
@success="() => getDeviceData(id)"
|
@success="() => getDeviceData(id)"
|
||||||
/>
|
/>
|
||||||
|
|
@ -151,7 +151,7 @@ onMounted(async () => {
|
||||||
label="Modbus 配置"
|
label="Modbus 配置"
|
||||||
>
|
>
|
||||||
<DeviceModbusConfig
|
<DeviceModbusConfig
|
||||||
v-if="activeTab === 'modbus'"
|
v-if="activeTab === 'modbus' && device.id"
|
||||||
:device="device"
|
:device="device"
|
||||||
:product="product"
|
:product="product"
|
||||||
:thing-model-list="thingModelList"
|
:thing-model-list="thingModelList"
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ defineProps<{ deviceId: number }>();
|
||||||
const dialogVisible = ref(false); // 弹窗的是否展示
|
const dialogVisible = ref(false); // 弹窗的是否展示
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const viewMode = ref<'chart' | 'list'>('chart'); // 视图模式状态
|
const viewMode = ref<'chart' | 'list'>('chart'); // 视图模式状态
|
||||||
const list = ref<IotDeviceApi.DevicePropertyDetail[]>([]); // 列表的数据
|
const list = ref<Array<IotDeviceApi.DeviceProperty & { _rowKey: string }>>([]); // 列表的数据
|
||||||
const total = ref(0); // 总数据量
|
const total = ref(0); // 总数据量
|
||||||
const thingModelDataType = ref<string>(''); // 物模型数据类型
|
const thingModelDataType = ref<string>(''); // 物模型数据类型
|
||||||
const propertyIdentifier = ref<string>(''); // 属性标识符
|
const propertyIdentifier = ref<string>(''); // 属性标识符
|
||||||
|
|
@ -118,9 +118,12 @@ function formatDateRangeWithTime(dates: [string, string]): [string, string] {
|
||||||
async function getList() {
|
async function getList() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
// 后端直接返回数组
|
// 后端直接返回数组,仅含 value/updateTime,给每行补 _rowKey 保证唯一
|
||||||
const data = await getHistoryDevicePropertyList(queryParams);
|
const data = await getHistoryDevicePropertyList(queryParams);
|
||||||
list.value = (data || []) as IotDeviceApi.DevicePropertyDetail[];
|
list.value = (data || []).map((item, idx) => ({
|
||||||
|
...item,
|
||||||
|
_rowKey: `${item.updateTime ?? ''}-${idx}`,
|
||||||
|
}));
|
||||||
total.value = list.value.length;
|
total.value = list.value.length;
|
||||||
|
|
||||||
// 如果是图表模式且支持图表展示,等待渲染图表
|
// 如果是图表模式且支持图表展示,等待渲染图表
|
||||||
|
|
@ -396,7 +399,7 @@ defineExpose({ open }); // 提供 open 方法,用于打开弹窗
|
||||||
<ElTable
|
<ElTable
|
||||||
:data="list"
|
:data="list"
|
||||||
:max-height="500"
|
:max-height="500"
|
||||||
row-key="updateTime"
|
row-key="_rowKey"
|
||||||
size="small"
|
size="small"
|
||||||
>
|
>
|
||||||
<ElTableColumn label="序号" width="80" align="center" type="index" />
|
<ElTableColumn label="序号" width="80" align="center" type="index" />
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,11 @@ const [Modal, modalApi] = useVbenModal({
|
||||||
|
|
||||||
/** 上传前 */
|
/** 上传前 */
|
||||||
function beforeUpload(file: File) {
|
function beforeUpload(file: File) {
|
||||||
|
const fileName = file.name?.toLowerCase() ?? '';
|
||||||
|
if (!fileName.endsWith('.xls') && !fileName.endsWith('.xlsx')) {
|
||||||
|
ElMessage.error('只能上传 Excel 文件(.xls / .xlsx)');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
formApi.setFieldValue('file', file);
|
formApi.setFieldValue('file', file);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,10 @@ function handleEdit(row: IotDeviceGroupApi.DeviceGroup) {
|
||||||
|
|
||||||
/** 删除设备分组 */
|
/** 删除设备分组 */
|
||||||
async function handleDelete(row: IotDeviceGroupApi.DeviceGroup) {
|
async function handleDelete(row: IotDeviceGroupApi.DeviceGroup) {
|
||||||
|
if (row.deviceCount && row.deviceCount > 0) {
|
||||||
|
ElMessage.warning(`分组「${row.name}」下存在 ${row.deviceCount} 台设备,无法删除`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const loadingInstance = ElLoading.service({
|
const loadingInstance = ElLoading.service({
|
||||||
text: $t('ui.actionMessage.deleting', [row.name]),
|
text: $t('ui.actionMessage.deleting', [row.name]),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -111,19 +111,20 @@ function initMap() {
|
||||||
// 信息窗口打开后绑定链接点击事件
|
// 信息窗口打开后绑定链接点击事件
|
||||||
infoWindow.addEventListener('open', () => {
|
infoWindow.addEventListener('open', () => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const link = document.querySelector('.device-link');
|
const link = document.querySelector('.BMap_bubble_content .device-link');
|
||||||
if (link) {
|
if (!link) {
|
||||||
link.addEventListener('click', (e) => {
|
return;
|
||||||
e.preventDefault();
|
|
||||||
const deviceId = (e.target as HTMLElement).dataset.id;
|
|
||||||
if (deviceId) {
|
|
||||||
router.push({
|
|
||||||
name: 'IoTDeviceDetail',
|
|
||||||
params: { id: deviceId },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
link.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const deviceId = (e.target as HTMLElement).dataset.id;
|
||||||
|
if (deviceId) {
|
||||||
|
router.push({
|
||||||
|
name: 'IoTDeviceDetail',
|
||||||
|
params: { id: deviceId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}, 100);
|
}, 100);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { onMounted, ref } from 'vue';
|
||||||
import { useAccess } from '@vben/access';
|
import { useAccess } from '@vben/access';
|
||||||
import { DICT_TYPE, ProductStatusEnum } from '@vben/constants';
|
import { DICT_TYPE, ProductStatusEnum } from '@vben/constants';
|
||||||
import { IconifyIcon } from '@vben/icons';
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
import { isHttpUrl } from '@vben/utils';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ElButton,
|
ElButton,
|
||||||
|
|
@ -111,7 +112,14 @@ onMounted(() => {
|
||||||
<div
|
<div
|
||||||
class="flex size-9 shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-[#40a9ff] to-[#1890ff] text-white"
|
class="flex size-9 shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-[#40a9ff] to-[#1890ff] text-white"
|
||||||
>
|
>
|
||||||
|
<img
|
||||||
|
v-if="isHttpUrl(item.icon)"
|
||||||
|
:src="item.icon"
|
||||||
|
alt=""
|
||||||
|
class="size-6 object-contain"
|
||||||
|
/>
|
||||||
<IconifyIcon
|
<IconifyIcon
|
||||||
|
v-else
|
||||||
:icon="item.icon || 'lucide:box'"
|
:icon="item.icon || 'lucide:box'"
|
||||||
class="text-xl"
|
class="text-xl"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -16,15 +16,18 @@ const props = defineProps<{
|
||||||
const emit = defineEmits(['update:modelValue']);
|
const emit = defineEmits(['update:modelValue']);
|
||||||
|
|
||||||
interface KeyValueItem {
|
interface KeyValueItem {
|
||||||
|
_uid: number;
|
||||||
key: string;
|
key: string;
|
||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let uidCounter = 0;
|
||||||
const items = ref<KeyValueItem[]>([]); // 内部 key-value 项列表
|
const items = ref<KeyValueItem[]>([]); // 内部 key-value 项列表
|
||||||
|
|
||||||
/** 添加项目 */
|
/** 添加项目 */
|
||||||
function addItem() {
|
function addItem() {
|
||||||
items.value.push({ key: '', value: '' });
|
uidCounter += 1;
|
||||||
|
items.value.push({ _uid: uidCounter, key: '', value: '' });
|
||||||
updateModelValue();
|
updateModelValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,16 +57,16 @@ watch(
|
||||||
if (isEmpty(val) || !isEmpty(items.value)) {
|
if (isEmpty(val) || !isEmpty(items.value)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
items.value = Object.entries(props.modelValue).map(([key, value]) => ({
|
items.value = Object.entries(props.modelValue).map(([key, value]) => {
|
||||||
key,
|
uidCounter += 1;
|
||||||
value,
|
return { _uid: uidCounter, key, value };
|
||||||
}));
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-for="(item, index) in items" :key="index" class="mb-2 flex w-full">
|
<div v-for="(item, index) in items" :key="item._uid" class="mb-2 flex w-full">
|
||||||
<ElInput v-model="item.key" class="mr-2" placeholder="键" />
|
<ElInput v-model="item.key" class="mr-2" placeholder="键" />
|
||||||
<ElInput v-model="item.value" placeholder="值" />
|
<ElInput v-model="item.value" placeholder="值" />
|
||||||
<ElButton class="ml-2" type="danger" link @click="removeItem(index)">
|
<ElButton class="ml-2" type="danger" link @click="removeItem(index)">
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, ref } from 'vue';
|
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
import { IconifyIcon } from '@vben/icons';
|
import { IconifyIcon } from '@vben/icons';
|
||||||
import { isEmpty } from '@vben/utils';
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
@ -30,14 +30,28 @@ const config = useVModel(props, 'modelValue', emit);
|
||||||
const showSqlTip = ref(false);
|
const showSqlTip = ref(false);
|
||||||
const copied = ref(false);
|
const copied = ref(false);
|
||||||
const { copy } = useClipboard();
|
const { copy } = useClipboard();
|
||||||
|
let copyResetTimer: null | ReturnType<typeof setTimeout> = null;
|
||||||
|
|
||||||
async function handleCopySql() {
|
async function handleCopySql() {
|
||||||
await copy(TABLE_SQL);
|
await copy(TABLE_SQL);
|
||||||
copied.value = true;
|
copied.value = true;
|
||||||
ElMessage.success('建表 SQL 已复制到剪贴板');
|
ElMessage.success('建表 SQL 已复制到剪贴板');
|
||||||
setTimeout(() => (copied.value = false), 2000);
|
if (copyResetTimer) {
|
||||||
|
clearTimeout(copyResetTimer);
|
||||||
|
}
|
||||||
|
copyResetTimer = setTimeout(() => {
|
||||||
|
copied.value = false;
|
||||||
|
copyResetTimer = null;
|
||||||
|
}, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (copyResetTimer) {
|
||||||
|
clearTimeout(copyResetTimer);
|
||||||
|
copyResetTimer = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!isEmpty(config.value)) {
|
if (!isEmpty(config.value)) {
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -131,15 +131,24 @@ function handleTriggerTypeChange(type: number) {
|
||||||
|
|
||||||
/** 处理产品变化事件 */
|
/** 处理产品变化事件 */
|
||||||
function handleProductChange() {
|
function handleProductChange() {
|
||||||
// 产品变化时清空设备和属性
|
const trigger = condition.value;
|
||||||
condition.value.deviceId = undefined;
|
trigger.deviceId = undefined;
|
||||||
condition.value.identifier = '';
|
trigger.identifier = '';
|
||||||
|
trigger.operator = undefined;
|
||||||
|
// 主条件比较值字段是 Trigger.value(不是 TriggerCondition.param)
|
||||||
|
trigger.value = '';
|
||||||
|
propertyType.value = '';
|
||||||
|
propertyConfig.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 处理设备变化事件 */
|
/** 处理设备变化事件 */
|
||||||
function handleDeviceChange() {
|
function handleDeviceChange() {
|
||||||
// 设备变化时清空属性
|
const trigger = condition.value;
|
||||||
condition.value.identifier = '';
|
trigger.identifier = '';
|
||||||
|
trigger.operator = undefined;
|
||||||
|
trigger.value = '';
|
||||||
|
propertyType.value = '';
|
||||||
|
propertyConfig.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -60,16 +60,26 @@ async function getDeviceList() {
|
||||||
// 监听产品变化
|
// 监听产品变化
|
||||||
watch(
|
watch(
|
||||||
() => props.productId,
|
() => props.productId,
|
||||||
(newProductId) => {
|
async (newProductId, oldProductId) => {
|
||||||
if (newProductId) {
|
if (!newProductId) {
|
||||||
getDeviceList();
|
|
||||||
} else {
|
|
||||||
deviceList.value = [];
|
deviceList.value = [];
|
||||||
// 清空当前选择的设备
|
if (props.modelValue !== undefined && props.modelValue !== null) {
|
||||||
if (props.modelValue) {
|
|
||||||
emit('update:modelValue', undefined);
|
emit('update:modelValue', undefined);
|
||||||
emit('change', undefined);
|
emit('change', undefined);
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await getDeviceList();
|
||||||
|
// 切换到新 productId 时,旧 deviceId 不在新列表里则清空
|
||||||
|
if (
|
||||||
|
oldProductId !== undefined &&
|
||||||
|
oldProductId !== newProductId &&
|
||||||
|
props.modelValue !== undefined &&
|
||||||
|
props.modelValue !== null &&
|
||||||
|
!deviceList.value.some((d: any) => d.id === props.modelValue)
|
||||||
|
) {
|
||||||
|
emit('update:modelValue', undefined);
|
||||||
|
emit('change', undefined);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
|
|
|
||||||
|
|
@ -186,10 +186,10 @@ function getNextExecutionTime(row: RuleSceneApi.SceneRule): Date | null {
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 基于当前页列表刷新统计数据 */
|
/** 刷新规则统计卡片数据 */
|
||||||
function updateStatistics(rows: RuleSceneApi.SceneRule[]) {
|
function updateStatistics(rows: RuleSceneApi.SceneRule[], total?: number) {
|
||||||
statistics.value = {
|
statistics.value = {
|
||||||
total: rows.length,
|
total: total ?? rows.length,
|
||||||
enabled: rows.filter((item) => item.status === CommonStatusEnum.ENABLE)
|
enabled: rows.filter((item) => item.status === CommonStatusEnum.ENABLE)
|
||||||
.length,
|
.length,
|
||||||
disabled: rows.filter((item) => item.status === CommonStatusEnum.DISABLE)
|
disabled: rows.filter((item) => item.status === CommonStatusEnum.DISABLE)
|
||||||
|
|
@ -214,7 +214,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
pageSize: page.pageSize,
|
pageSize: page.pageSize,
|
||||||
...formValues,
|
...formValues,
|
||||||
});
|
});
|
||||||
updateStatistics(result.list || []);
|
updateStatistics(result.list || [], result.total);
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -237,7 +237,11 @@ function removeDataSpecs(val: any) {
|
||||||
label="标识符"
|
label="标识符"
|
||||||
prop="identifier"
|
prop="identifier"
|
||||||
>
|
>
|
||||||
<ElInput v-model="formData.identifier" placeholder="请输入标识符" />
|
<ElInput
|
||||||
|
v-model="formData.identifier"
|
||||||
|
:disabled="formData.id != null"
|
||||||
|
placeholder="请输入标识符"
|
||||||
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<!-- 属性配置 -->
|
<!-- 属性配置 -->
|
||||||
<ThingModelProperty
|
<ThingModelProperty
|
||||||
|
|
|
||||||
|
|
@ -97,3 +97,22 @@ export const setCurrentTimezone = (timezone?: string) => {
|
||||||
export const getCurrentTimezone = () => {
|
export const getCurrentTimezone = () => {
|
||||||
return currentTimezone;
|
return currentTimezone;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把 antd TimePicker / DatePicker `@update:value` 回传的值统一成字符串。
|
||||||
|
*
|
||||||
|
* antd 在设置了 `value-format` 后实际只会回传字符串,
|
||||||
|
* 但 `@update:value` 的类型仍包含 `Dayjs`,调用方需要做一次类型归一。
|
||||||
|
*
|
||||||
|
* - 空值(null / undefined / '' / 0)返回 ''
|
||||||
|
* - 已经是字符串:原样返回(保持 `value-format` 已格式化的结果)
|
||||||
|
* - 兜底的 Dayjs:调用 `.format()` 转默认 ISO 字符串
|
||||||
|
*/
|
||||||
|
export function formatDayjs(
|
||||||
|
value: dayjs.Dayjs | null | string | undefined,
|
||||||
|
): string {
|
||||||
|
if (!value) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return typeof value === 'string' ? value : value.format();
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -831,6 +831,9 @@ importers:
|
||||||
tinymce:
|
tinymce:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 7.9.2
|
version: 7.9.2
|
||||||
|
tyme4ts:
|
||||||
|
specifier: ^1.5.0
|
||||||
|
version: 1.5.0
|
||||||
video.js:
|
video.js:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 7.21.7
|
version: 7.21.7
|
||||||
|
|
@ -1083,6 +1086,9 @@ importers:
|
||||||
tinymce:
|
tinymce:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 7.9.2
|
version: 7.9.2
|
||||||
|
tyme4ts:
|
||||||
|
specifier: ^1.5.0
|
||||||
|
version: 1.5.0
|
||||||
video.js:
|
video.js:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 7.21.7
|
version: 7.21.7
|
||||||
|
|
@ -11536,6 +11542,9 @@ packages:
|
||||||
tw-animate-css@1.4.0:
|
tw-animate-css@1.4.0:
|
||||||
resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==}
|
resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==}
|
||||||
|
|
||||||
|
tyme4ts@1.5.0:
|
||||||
|
resolution: {integrity: sha512-SqmlNyDtYb3bsnSkjX8lxyMcCt9xBaBkF8xWs/2ORiysWXftEoTGHEi/zxWEJ8s7aANe5veMcLcOuMK6Z+kzbw==}
|
||||||
|
|
||||||
type-check@0.4.0:
|
type-check@0.4.0:
|
||||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
@ -22397,6 +22406,8 @@ snapshots:
|
||||||
|
|
||||||
tw-animate-css@1.4.0: {}
|
tw-animate-css@1.4.0: {}
|
||||||
|
|
||||||
|
tyme4ts@1.5.0: {}
|
||||||
|
|
||||||
type-check@0.4.0:
|
type-check@0.4.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
prelude-ls: 1.2.1
|
prelude-ls: 1.2.1
|
||||||
|
|
|
||||||
|
|
@ -194,6 +194,7 @@ catalog:
|
||||||
tsdown: ^0.21.7
|
tsdown: ^0.21.7
|
||||||
turbo: ^2.9.6
|
turbo: ^2.9.6
|
||||||
tw-animate-css: ^1.4.0
|
tw-animate-css: ^1.4.0
|
||||||
|
tyme4ts: ^1.5.0
|
||||||
typescript: ^6.0.2
|
typescript: ^6.0.2
|
||||||
unplugin-dts: ^1.0.0-beta.6
|
unplugin-dts: ^1.0.0-beta.6
|
||||||
unplugin-element-plus: ^0.11.2
|
unplugin-element-plus: ^0.11.2
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue