This commit is contained in:
xingyu4j
2025-10-11 10:56:12 +08:00
parent 28566a659f
commit a156873437
30 changed files with 471 additions and 501 deletions

View File

@@ -154,7 +154,7 @@ async function handleDelete(row: any) {
}); });
try { try {
await deleteDevice(row.id); await deleteDevice(row.id);
message.success($t('common.delSuccess')); message.success($t('ui.actionMessage.deleteSuccess'));
handleRefresh(); handleRefresh();
} finally { } finally {
hideLoading(); hideLoading();
@@ -175,7 +175,7 @@ async function handleDeleteBatch() {
try { try {
const ids = checkedRows.map((row: any) => row.id); const ids = checkedRows.map((row: any) => row.id);
await deleteDeviceList(ids); await deleteDeviceList(ids);
message.success($t('common.delSuccess')); message.success($t('ui.actionMessage.deleteSuccess'));
handleRefresh(); handleRefresh();
} finally { } finally {
hideLoading(); hideLoading();

View File

@@ -102,7 +102,7 @@ export function useIotHome() {
} }
/** 格式化数字 - 大数字显示为 K/M */ /** 格式化数字 - 大数字显示为 K/M */
export const formatNumber = (num: number): string => { export function formatNumber(num: number): string {
if (num >= 1_000_000) { if (num >= 1_000_000) {
return `${(num / 1_000_000).toFixed(1)}M`; return `${(num / 1_000_000).toFixed(1)}M`;
} }
@@ -110,4 +110,4 @@ export const formatNumber = (num: number): string => {
return `${(num / 1000).toFixed(1)}K`; return `${(num / 1000).toFixed(1)}K`;
} }
return num.toString(); return num.toString();
}; }

View File

@@ -1,12 +1,7 @@
import type { VbenFormSchema } from '#/adapter/form'; import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { IoTOtaFirmwareApi } from '#/api/iot/ota/firmware';
import { message } from 'ant-design-vue';
import { deleteOtaFirmware, getOtaFirmwarePage } from '#/api/iot/ota/firmware';
import { getSimpleProductList } from '#/api/iot/product/product'; import { getSimpleProductList } from '#/api/iot/product/product';
import { $t } from '#/locales';
import { getRangePickerDefaultProps } from '#/utils'; import { getRangePickerDefaultProps } from '#/utils';
/** 新增/修改固件的表单 */ /** 新增/修改固件的表单 */
@@ -157,51 +152,3 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
}, },
]; ];
} }
/** Grid 配置项 */
export function useGridOptions(): VxeTableGridOptions<IoTOtaFirmwareApi.Firmware> {
return {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getOtaFirmwarePage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
search: true,
},
};
}
/** 删除固件 */
export async function handleDeleteFirmware(
row: IoTOtaFirmwareApi.Firmware,
onSuccess: () => void,
) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
});
try {
await deleteOtaFirmware(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
});
onSuccess();
} finally {
hideLoading();
}
}

View File

@@ -1,17 +1,17 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { IoTOtaFirmwareApi } from '#/api/iot/ota/firmware'; import type { IoTOtaFirmwareApi } from '#/api/iot/ota/firmware';
import { Page, useVbenModal } from '@vben/common-ui'; import { Page, useVbenModal } from '@vben/common-ui';
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 { deleteOtaFirmware, getOtaFirmwarePage } from '#/api/iot/ota/firmware';
import { $t } from '#/locales'; import { $t } from '#/locales';
import Form from '../modules/OtaFirmwareForm.vue'; import Form from '../modules/OtaFirmwareForm.vue';
import { import { useGridColumns, useGridFormSchema } from './data';
handleDeleteFirmware,
useGridFormSchema,
useGridOptions,
} from './data';
defineOptions({ name: 'IoTOtaFirmware' }); defineOptions({ name: 'IoTOtaFirmware' });
@@ -35,21 +35,56 @@ function handleEdit(row: IoTOtaFirmwareApi.Firmware) {
formModalApi.setData({ type: 'update', id: row.id }).open(); formModalApi.setData({ type: 'update', id: row.id }).open();
} }
/** 删除固件 */
async function handleDelete(row: IoTOtaFirmwareApi.Firmware) {
await handleDeleteFirmware(row, onRefresh);
}
/** 查看固件详情 */ /** 查看固件详情 */
function handleDetail(row: IoTOtaFirmwareApi.Firmware) { function handleDetail(row: IoTOtaFirmwareApi.Firmware) {
formModalApi.setData({ type: 'view', id: row.id }).open(); formModalApi.setData({ type: 'view', id: row.id }).open();
} }
/** 删除固件 */
async function handleDelete(row: IoTOtaFirmwareApi.Firmware) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
});
try {
await deleteOtaFirmware(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
});
onRefresh();
} finally {
hideLoading();
}
}
const [Grid, gridApi] = useVbenVxeGrid({ const [Grid, gridApi] = useVbenVxeGrid({
formOptions: { formOptions: {
schema: useGridFormSchema(), schema: useGridFormSchema(),
}, },
gridOptions: useGridOptions(), gridOptions: {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getOtaFirmwarePage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
search: true,
},
} as VxeTableGridOptions<IoTOtaFirmwareApi.Firmware>,
}); });
</script> </script>

View File

@@ -27,17 +27,17 @@ const firmwareStatisticsLoading = ref(false);
const firmwareStatistics = ref<Record<string, number>>({}); const firmwareStatistics = ref<Record<string, number>>({});
/** 获取固件信息 */ /** 获取固件信息 */
const getFirmwareInfo = async () => { async function getFirmwareInfo() {
firmwareLoading.value = true; firmwareLoading.value = true;
try { try {
firmware.value = await IoTOtaFirmwareApi.getOtaFirmware(firmwareId.value); firmware.value = await IoTOtaFirmwareApi.getOtaFirmware(firmwareId.value);
} finally { } finally {
firmwareLoading.value = false; firmwareLoading.value = false;
} }
}; }
/** 获取升级统计 */ /** 获取升级统计 */
const getStatistics = async () => { async function getStatistics() {
firmwareStatisticsLoading.value = true; firmwareStatisticsLoading.value = true;
try { try {
firmwareStatistics.value = firmwareStatistics.value =
@@ -47,7 +47,7 @@ const getStatistics = async () => {
} finally { } finally {
firmwareStatisticsLoading.value = false; firmwareStatisticsLoading.value = false;
} }
}; }
/** 初始化 */ /** 初始化 */
onMounted(() => { onMounted(() => {

View File

@@ -27,17 +27,17 @@ const firmwareStatisticsLoading = ref(false);
const firmwareStatistics = ref<Record<string, number>>({}); const firmwareStatistics = ref<Record<string, number>>({});
/** 获取固件信息 */ /** 获取固件信息 */
const getFirmwareInfo = async () => { async function getFirmwareInfo() {
firmwareLoading.value = true; firmwareLoading.value = true;
try { try {
firmware.value = await IoTOtaFirmwareApi.getOtaFirmware(firmwareId.value); firmware.value = await IoTOtaFirmwareApi.getOtaFirmware(firmwareId.value);
} finally { } finally {
firmwareLoading.value = false; firmwareLoading.value = false;
} }
}; }
/** 获取升级统计 */ /** 获取升级统计 */
const getStatistics = async () => { async function getStatistics() {
firmwareStatisticsLoading.value = true; firmwareStatisticsLoading.value = true;
try { try {
firmwareStatistics.value = firmwareStatistics.value =
@@ -47,7 +47,7 @@ const getStatistics = async () => {
} finally { } finally {
firmwareStatisticsLoading.value = false; firmwareStatisticsLoading.value = false;
} }
}; }
/** 初始化 */ /** 初始化 */
onMounted(() => { onMounted(() => {

View File

@@ -113,7 +113,7 @@ const columns: TableColumnsType = [
const [ModalComponent, modalApi] = useVbenModal(); const [ModalComponent, modalApi] = useVbenModal();
/** 获取任务详情 */ /** 获取任务详情 */
const getTaskInfo = async () => { async function getTaskInfo() {
if (!taskId.value) { if (!taskId.value) {
return; return;
} }
@@ -123,10 +123,10 @@ const getTaskInfo = async () => {
} finally { } finally {
taskLoading.value = false; taskLoading.value = false;
} }
}; }
/** 获取统计数据 */ /** 获取统计数据 */
const getStatistics = async () => { async function getStatistics() {
if (!taskId.value) { if (!taskId.value) {
return; return;
} }
@@ -140,10 +140,10 @@ const getStatistics = async () => {
} finally { } finally {
taskStatisticsLoading.value = false; taskStatisticsLoading.value = false;
} }
}; }
/** 获取升级记录列表 */ /** 获取升级记录列表 */
const getRecordList = async () => { async function getRecordList() {
if (!taskId.value) { if (!taskId.value) {
return; return;
} }
@@ -156,26 +156,26 @@ const getRecordList = async () => {
} finally { } finally {
recordLoading.value = false; recordLoading.value = false;
} }
}; }
/** 切换标签 */ /** 切换标签 */
const handleTabChange = (tabKey: number | string) => { function handleTabChange(tabKey: number | string) {
activeTab.value = String(tabKey); activeTab.value = String(tabKey);
queryParams.pageNo = 1; queryParams.pageNo = 1;
queryParams.status = queryParams.status =
activeTab.value === '' ? undefined : Number.parseInt(String(tabKey)); activeTab.value === '' ? undefined : Number.parseInt(String(tabKey));
getRecordList(); getRecordList();
}; }
/** 分页变化 */ /** 分页变化 */
const handleTableChange = (pagination: any) => { function handleTableChange(pagination: any) {
queryParams.pageNo = pagination.current; queryParams.pageNo = pagination.current;
queryParams.pageSize = pagination.pageSize; queryParams.pageSize = pagination.pageSize;
getRecordList(); getRecordList();
}; }
/** 取消升级 */ /** 取消升级 */
const handleCancelUpgrade = async (record: OtaTaskRecord) => { async function handleCancelUpgrade(record: OtaTaskRecord) {
Modal.confirm({ Modal.confirm({
title: '确认取消', title: '确认取消',
content: '确认要取消该设备的升级任务吗?', content: '确认要取消该设备的升级任务吗?',
@@ -192,10 +192,10 @@ const handleCancelUpgrade = async (record: OtaTaskRecord) => {
} }
}, },
}); });
}; }
/** 打开弹窗 */ /** 打开弹窗 */
const open = (id: number) => { function open(id: number) {
modalApi.open(); modalApi.open();
taskId.value = id; taskId.value = id;
activeTab.value = ''; activeTab.value = '';
@@ -206,7 +206,7 @@ const open = (id: number) => {
getTaskInfo(); getTaskInfo();
getStatistics(); getStatistics();
getRecordList(); getRecordList();
}; }
/** 暴露方法 */ /** 暴露方法 */
defineExpose({ open }); defineExpose({ open });

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { DeviceVO } from '#/api/iot/device/device'; import type { IotDeviceApi } from '#/api/iot/device/device';
import type { OtaTask } from '#/api/iot/ota/task'; import type { OtaTask } from '#/api/iot/ota/task';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
@@ -57,7 +57,7 @@ const formRules = {
}, },
], ],
}; };
const devices = ref<DeviceVO[]>([]); const devices = ref<IotDeviceApi.Device[]>([]);
/** 设备选项 */ /** 设备选项 */
const deviceOptions = computed(() => { const deviceOptions = computed(() => {
@@ -107,7 +107,7 @@ const [Modal, modalApi] = useVbenModal({
}); });
/** 重置表单 */ /** 重置表单 */
const resetForm = () => { function resetForm() {
formData.value = { formData.value = {
name: '', name: '',
deviceScope: IoTOtaTaskDeviceScopeEnum.ALL.value, deviceScope: IoTOtaTaskDeviceScopeEnum.ALL.value,
@@ -116,12 +116,12 @@ const resetForm = () => {
deviceIds: [], deviceIds: [],
}; };
formRef.value?.resetFields(); formRef.value?.resetFields();
}; }
/** 打开弹窗 */ /** 打开弹窗 */
const open = async () => { async function open() {
await modalApi.open(); await modalApi.open();
}; }
defineExpose({ open }); defineExpose({ open });
</script> </script>

View File

@@ -40,7 +40,7 @@ const taskFormRef = ref(); // 任务表单引用
const taskDetailRef = ref(); // 任务详情引用 const taskDetailRef = ref(); // 任务详情引用
/** 获取任务列表 */ /** 获取任务列表 */
const getTaskList = async () => { async function getTaskList() {
taskLoading.value = true; taskLoading.value = true;
try { try {
const data = await IoTOtaTaskApi.getOtaTaskPage(queryParams); const data = await IoTOtaTaskApi.getOtaTaskPage(queryParams);
@@ -49,32 +49,32 @@ const getTaskList = async () => {
} finally { } finally {
taskLoading.value = false; taskLoading.value = false;
} }
}; }
/** 搜索 */ /** 搜索 */
const handleQuery = () => { function handleQuery() {
queryParams.pageNo = 1; queryParams.pageNo = 1;
getTaskList(); getTaskList();
}; }
/** 打开任务表单 */ /** 打开任务表单 */
const openTaskForm = () => { function openTaskForm() {
taskFormRef.value?.open(); taskFormRef.value?.open();
}; }
/** 处理任务创建成功 */ /** 处理任务创建成功 */
const handleTaskCreateSuccess = () => { function handleTaskCreateSuccess() {
getTaskList(); getTaskList();
emit('success'); emit('success');
}; }
/** 查看任务详情 */ /** 查看任务详情 */
const handleTaskDetail = (id: number) => { function handleTaskDetail(id: number) {
taskDetailRef.value?.open(id); taskDetailRef.value?.open(id);
}; }
/** 取消任务 */ /** 取消任务 */
const handleCancelTask = async (id: number) => { async function handleCancelTask(id: number) {
Modal.confirm({ Modal.confirm({
title: '确认取消', title: '确认取消',
content: '确认要取消该升级任务吗?', content: '确认要取消该升级任务吗?',
@@ -88,20 +88,20 @@ const handleCancelTask = async (id: number) => {
} }
}, },
}); });
}; }
/** 刷新数据 */ /** 刷新数据 */
const refresh = async () => { async function refresh() {
await getTaskList(); await getTaskList();
emit('success'); emit('success');
}; }
/** 分页变化 */ /** 分页变化 */
const handleTableChange = (pagination: any) => { function handleTableChange(pagination: any) {
queryParams.pageNo = pagination.current; queryParams.pageNo = pagination.current;
queryParams.pageSize = pagination.pageSize; queryParams.pageSize = pagination.pageSize;
getTaskList(); getTaskList();
}; }
/** 表格列配置 */ /** 表格列配置 */
const columns: TableColumnsType = [ const columns: TableColumnsType = [

View File

@@ -1,19 +1,10 @@
import type { VbenFormSchema } from '#/adapter/form'; import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { IotProductCategoryApi } from '#/api/iot/product/category';
import { DICT_TYPE } from '@vben/constants'; import { DICT_TYPE } from '@vben/constants';
import { handleTree } from '@vben/utils';
import { message } from 'ant-design-vue';
import { z } from '#/adapter/form'; import { z } from '#/adapter/form';
import { import { getSimpleProductCategoryList } from '#/api/iot/product/category';
deleteProductCategory,
getProductCategoryPage,
getSimpleProductCategoryList,
} from '#/api/iot/product/category';
import { $t } from '#/locales';
/** 新增/修改产品分类的表单 */ /** 新增/修改产品分类的表单 */
export function useFormSchema(): VbenFormSchema[] { export function useFormSchema(): VbenFormSchema[] {
@@ -160,35 +151,3 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
}, },
]; ];
} }
/** 删除分类 */
export async function handleDeleteCategory(
row: IotProductCategoryApi.ProductCategory,
onSuccess?: () => void,
) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
});
try {
await deleteProductCategory(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
onSuccess?.();
} finally {
hideLoading();
}
}
/** 查询分类列表 */
export async function queryProductCategoryList({ page }: any, formValues: any) {
const data = await getProductCategoryPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
// 转换为树形结构
return {
...data,
list: handleTree(data.list, 'id', 'parentId'),
};
}

View File

@@ -3,16 +3,18 @@ import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { IotProductCategoryApi } from '#/api/iot/product/category'; import type { IotProductCategoryApi } from '#/api/iot/product/category';
import { Page, useVbenModal } from '@vben/common-ui'; import { Page, useVbenModal } from '@vben/common-ui';
import { handleTree } from '@vben/utils';
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 {
deleteProductCategory,
getProductCategoryPage,
} from '#/api/iot/product/category';
import { $t } from '#/locales'; import { $t } from '#/locales';
import { import { useGridColumns, useGridFormSchema } from './data';
handleDeleteCategory,
queryProductCategoryList,
useGridColumns,
useGridFormSchema,
} from './data';
import Form from './modules/ProductCategoryForm.vue'; import Form from './modules/ProductCategoryForm.vue';
defineOptions({ name: 'IoTProductCategory' }); defineOptions({ name: 'IoTProductCategory' });
@@ -39,7 +41,17 @@ function handleEdit(row: IotProductCategoryApi.ProductCategory) {
/** 删除分类 */ /** 删除分类 */
async function handleDelete(row: IotProductCategoryApi.ProductCategory) { async function handleDelete(row: IotProductCategoryApi.ProductCategory) {
await handleDeleteCategory(row, handleRefresh); const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
});
try {
await deleteProductCategory(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
handleRefresh();
} finally {
hideLoading();
}
} }
const [Grid, gridApi] = useVbenVxeGrid({ const [Grid, gridApi] = useVbenVxeGrid({
@@ -57,7 +69,18 @@ const [Grid, gridApi] = useVbenVxeGrid({
}, },
proxyConfig: { proxyConfig: {
ajax: { ajax: {
query: queryProductCategoryList, query: async ({ page }, formValues) => {
const data = await getProductCategoryPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
// 转换为树形结构
return {
...data,
list: handleTree(data.list, 'id', 'parentId'),
};
},
}, },
}, },
rowConfig: { rowConfig: {

View File

@@ -5,17 +5,10 @@ import { ref } from 'vue';
import { DICT_TYPE } from '@vben/constants'; import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks'; import { getDictOptions } from '@vben/hooks';
import { downloadFileFromBlobPart } from '@vben/utils';
import { message } from 'ant-design-vue';
import { z } from '#/adapter/form'; import { z } from '#/adapter/form';
import { getSimpleProductCategoryList } from '#/api/iot/product/category'; import { getSimpleProductCategoryList } from '#/api/iot/product/category';
import { import { getProductPage } from '#/api/iot/product/product';
deleteProduct,
exportProduct,
getProductPage,
} from '#/api/iot/product/product';
/** 新增/修改产品的表单 */ /** 新增/修改产品的表单 */
export function useFormSchema(): VbenFormSchema[] { export function useFormSchema(): VbenFormSchema[] {
@@ -208,38 +201,6 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
]; ];
} }
/** 加载产品分类列表 */
export async function loadCategoryList() {
return await getSimpleProductCategoryList();
}
/** 获取分类名称 */
export function getCategoryName(categoryList: any[], categoryId: number) {
const category = categoryList.find((c: any) => c.id === categoryId);
return category?.name || '未分类';
}
/** 删除产品 */
export async function handleDeleteProduct(row: any, onSuccess?: () => void) {
const hideLoading = message.loading({
content: `正在删除 ${row.name}...`,
duration: 0,
});
try {
await deleteProduct(row.id);
message.success(`删除 ${row.name} 成功`);
onSuccess?.();
} finally {
hideLoading();
}
}
/** 导出产品 */
export async function handleExportProduct(searchParams: any) {
const data = await exportProduct(searchParams);
downloadFileFromBlobPart({ fileName: '产品列表.xls', source: data });
}
/** 查询产品列表 */ /** 查询产品列表 */
export async function queryProductList({ page }: any, searchParams: any) { export async function queryProductList({ page }: any, searchParams: any) {
return await getProductPage({ return await getProductPage({

View File

@@ -6,21 +6,20 @@ import { useRouter } from 'vue-router';
import { Page, useVbenModal } from '@vben/common-ui'; import { Page, useVbenModal } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons'; import { IconifyIcon } from '@vben/icons';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, Card, Image, Input, Space } from 'ant-design-vue'; import { Button, Card, Image, Input, message, Space } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteProduct,
exportProduct,
getProductPage,
} from '#/api/crm/product';
import { getSimpleProductCategoryList } from '#/api/iot/product/category';
import { $t } from '#/locales'; import { $t } from '#/locales';
import { import { useGridColumns, useImagePreview } from './data';
getCategoryName,
handleDeleteProduct,
handleExportProduct,
loadCategoryList,
queryProductList,
useGridColumns,
useImagePreview,
} from './data';
// @ts-ignore // @ts-ignore
import ProductCardView from './modules/ProductCardView.vue'; import ProductCardView from './modules/ProductCardView.vue';
import ProductForm from './modules/ProductForm.vue'; import ProductForm from './modules/ProductForm.vue';
@@ -47,14 +46,15 @@ const [FormModal, formModalApi] = useVbenModal({
}); });
// 加载产品分类列表 // 加载产品分类列表
const loadCategories = async () => { async function loadCategories() {
categoryList.value = await loadCategoryList(); categoryList.value = await getSimpleProductCategoryList();
}; }
// 获取分类名称 // 获取分类名称
const getCategoryNameByValue = (categoryId: number) => { function getCategoryNameByValue(categoryId: number) {
return getCategoryName(categoryList.value, categoryId); const category = categoryList.value.find((c: any) => c.id === categoryId);
}; return category?.name || '未分类';
}
/** 搜索 */ /** 搜索 */
function handleSearch() { function handleSearch() {
@@ -84,7 +84,8 @@ function handleRefresh() {
/** 导出表格 */ /** 导出表格 */
async function handleExport() { async function handleExport() {
await handleExportProduct(searchParams.value); const data = await exportProduct(searchParams.value);
await downloadFileFromBlobPart({ fileName: '产品列表.xls', source: data });
} }
/** 打开产品详情 */ /** 打开产品详情 */
@@ -116,7 +117,17 @@ function handleEdit(row: any) {
/** 删除产品 */ /** 删除产品 */
async function handleDelete(row: any) { async function handleDelete(row: any) {
await handleDeleteProduct(row, handleRefresh); const hideLoading = message.loading({
content: `正在删除 ${row.name}...`,
duration: 0,
});
try {
await deleteProduct(row.id);
message.success(`删除 ${row.name} 成功`);
handleRefresh();
} finally {
hideLoading();
}
} }
const [Grid, gridApi] = useVbenVxeGrid({ const [Grid, gridApi] = useVbenVxeGrid({
@@ -129,7 +140,13 @@ const [Grid, gridApi] = useVbenVxeGrid({
keepSource: true, keepSource: true,
proxyConfig: { proxyConfig: {
ajax: { ajax: {
query: ({ page }) => queryProductList({ page }, searchParams.value), query: async ({ page }) => {
return await getProductPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...searchParams.value,
});
},
}, },
}, },
rowConfig: { rowConfig: {
@@ -336,6 +353,6 @@ onMounted(() => {
} }
.ant-image-preview-operations { .ant-image-preview-operations {
background: rgba(0, 0, 0, 0.7) !important; background: rgb(0 0 0 / 70%) !important;
} }
</style> </style>

View File

@@ -48,13 +48,13 @@ const queryParams = ref({
}); });
// 获取分类名称 // 获取分类名称
const getCategoryName = (categoryId: number) => { function getCategoryName(categoryId: number) {
const category = props.categoryList.find((c: any) => c.id === categoryId); const category = props.categoryList.find((c: any) => c.id === categoryId);
return category?.name || '未分类'; return category?.name || '未分类';
}; }
// 获取产品列表 // 获取产品列表
const getList = async () => { async function getList() {
loading.value = true; loading.value = true;
try { try {
const data = await getProductPage({ const data = await getProductPage({
@@ -66,23 +66,23 @@ const getList = async () => {
} finally { } finally {
loading.value = false; loading.value = false;
} }
}; }
// 处理页码变化 // 处理页码变化
const handlePageChange = (page: number, pageSize: number) => { function handlePageChange(page: number, pageSize: number) {
queryParams.value.pageNo = page; queryParams.value.pageNo = page;
queryParams.value.pageSize = pageSize; queryParams.value.pageSize = pageSize;
getList(); getList();
}; }
// 获取设备类型颜色 // 获取设备类型颜色
const getDeviceTypeColor = (deviceType: number) => { function getDeviceTypeColor(deviceType: number) {
const colors: Record<number, string> = { const colors: Record<number, string> = {
0: 'blue', 0: 'blue',
1: 'green', 1: 'green',
}; };
return colors[deviceType] || 'default'; return colors[deviceType] || 'default';
}; }
onMounted(() => { onMounted(() => {
getList(); getList();
@@ -131,9 +131,9 @@ defineExpose({
<div class="info-list flex-1"> <div class="info-list flex-1">
<div class="info-item"> <div class="info-item">
<span class="info-label">产品分类</span> <span class="info-label">产品分类</span>
<span class="info-value text-primary">{{ <span class="info-value text-primary">
getCategoryName(item.categoryId) {{ getCategoryName(item.categoryId) }}
}}</span> </span>
</div> </div>
<div class="info-item"> <div class="info-item">
<span class="info-label">产品类型</span> <span class="info-label">产品类型</span>
@@ -152,9 +152,9 @@ defineExpose({
<div class="info-item"> <div class="info-item">
<span class="info-label">产品标识</span> <span class="info-label">产品标识</span>
<Tooltip :title="item.productKey || item.id" placement="top"> <Tooltip :title="item.productKey || item.id" placement="top">
<span class="info-value product-key">{{ <span class="info-value product-key">
item.productKey || item.id {{ item.productKey || item.id }}
}}</span> </span>
</Tooltip> </Tooltip>
</div> </div>
</div> </div>
@@ -236,44 +236,44 @@ defineExpose({
.product-card-view { .product-card-view {
.product-card { .product-card {
height: 100%; height: 100%;
transition: all 0.3s ease; overflow: hidden;
border: 1px solid #e8e8e8; border: 1px solid #e8e8e8;
border-radius: 8px; border-radius: 8px;
overflow: hidden; transition: all 0.3s ease;
&:hover { &:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
border-color: #d9d9d9; border-color: #d9d9d9;
box-shadow: 0 4px 16px rgb(0 0 0 / 8%);
transform: translateY(-2px); transform: translateY(-2px);
} }
:deep(.ant-card-body) { :deep(.ant-card-body) {
height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%;
} }
// 产品图标 // 产品图标
.product-icon { .product-icon {
width: 48px;
height: 48px;
display: flex; display: flex;
flex-shrink: 0;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 48px;
height: 48px;
color: white;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 8px; border-radius: 8px;
color: white;
flex-shrink: 0;
} }
// 产品标题 // 产品标题
.product-title { .product-title {
font-size: 16px;
font-weight: 600;
color: #1f2937;
line-height: 1.5;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
font-size: 16px;
font-weight: 600;
line-height: 1.5;
color: #1f2937;
white-space: nowrap; white-space: nowrap;
} }
@@ -290,16 +290,16 @@ defineExpose({
} }
.info-label { .info-label {
color: #6b7280;
margin-right: 8px;
flex-shrink: 0; flex-shrink: 0;
margin-right: 8px;
color: #6b7280;
} }
.info-value { .info-value {
color: #1f2937;
font-weight: 500;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
font-weight: 500;
color: #1f2937;
white-space: nowrap; white-space: nowrap;
&.text-primary { &.text-primary {
@@ -308,15 +308,15 @@ defineExpose({
} }
.product-key { .product-key {
font-family: 'Courier New', monospace;
font-size: 12px;
color: #374151;
display: inline-block; display: inline-block;
max-width: 150px; max-width: 150px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; font-family: 'Courier New', monospace;
font-size: 12px;
vertical-align: middle; vertical-align: middle;
color: #374151;
white-space: nowrap;
cursor: help; cursor: help;
} }
@@ -328,15 +328,15 @@ defineExpose({
// 3D 图标 // 3D 图标
.product-3d-icon { .product-3d-icon {
width: 100px;
height: 100px;
display: flex; display: flex;
flex-shrink: 0;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 100px;
height: 100px;
color: #667eea;
background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%); background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%);
border-radius: 8px; border-radius: 8px;
flex-shrink: 0;
color: #667eea;
} }
// 按钮组 // 按钮组
@@ -344,8 +344,8 @@ defineExpose({
display: flex; display: flex;
gap: 8px; gap: 8px;
padding-top: 12px; padding-top: 12px;
border-top: 1px solid #f0f0f0;
margin-top: auto; margin-top: auto;
border-top: 1px solid #f0f0f0;
.action-btn { .action-btn {
flex: 1; flex: 1;
@@ -359,8 +359,8 @@ defineExpose({
border-color: #1890ff; border-color: #1890ff;
&:hover { &:hover {
background: #1890ff;
color: white; color: white;
background: #1890ff;
} }
} }
@@ -369,8 +369,8 @@ defineExpose({
border-color: #52c41a; border-color: #52c41a;
&:hover { &:hover {
background: #52c41a;
color: white; color: white;
background: #52c41a;
} }
} }
@@ -379,8 +379,8 @@ defineExpose({
border-color: #722ed1; border-color: #722ed1;
&:hover { &:hover {
background: #722ed1;
color: white; color: white;
background: #722ed1;
} }
} }

View File

@@ -5,8 +5,9 @@ import type { IotProductApi } from '#/api/iot/product/product';
import { reactive, ref } from 'vue'; import { reactive, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui'; import { useVbenModal } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
import { message } from 'ant-design-vue'; import { Button, Form, Input, message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table'; import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getProductPage } from '#/api/iot/product/product'; import { getProductPage } from '#/api/iot/product/product';
@@ -101,24 +102,24 @@ const [Grid, gridApi] = useVbenVxeGrid({
}); });
// 打开选择器 // 打开选择器
const open = async () => { async function open() {
selectedProducts.value = []; selectedProducts.value = [];
selectedRowKeys.value = []; selectedRowKeys.value = [];
modalApi.open(); modalApi.open();
gridApi.reload(); gridApi.reload();
}; }
// 搜索 // 搜索
const handleSearch = () => { function handleSearch() {
gridApi.reload(); gridApi.reload();
}; }
// 重置搜索 // 重置搜索
const handleReset = () => { function handleReset() {
queryParams.name = ''; queryParams.name = '';
queryParams.productKey = ''; queryParams.productKey = '';
gridApi.reload(); gridApi.reload();
}; }
// 确认选择 // 确认选择
async function handleConfirm() { async function handleConfirm() {
@@ -151,40 +152,40 @@ defineExpose({ open });
<template> <template>
<Modal class="!w-[900px]"> <Modal class="!w-[900px]">
<div class="mb-4"> <div class="mb-4">
<a-form layout="inline" :model="queryParams"> <Form layout="inline" :model="queryParams">
<a-form-item label="产品名称"> <Form.Item label="产品名称">
<a-input <Input
v-model:value="queryParams.name" v-model:value="queryParams.name"
placeholder="请输入产品名称" placeholder="请输入产品名称"
allow-clear allow-clear
class="!w-[200px]" class="!w-[200px]"
@press-enter="handleSearch" @press-enter="handleSearch"
/> />
</a-form-item> </Form.Item>
<a-form-item label="ProductKey"> <Form.Item label="ProductKey">
<a-input <Input
v-model:value="queryParams.productKey" v-model:value="queryParams.productKey"
placeholder="请输入产品标识" placeholder="请输入产品标识"
allow-clear allow-clear
class="!w-[200px]" class="!w-[200px]"
@press-enter="handleSearch" @press-enter="handleSearch"
/> />
</a-form-item> </Form.Item>
<a-form-item> <Form.Item>
<a-button type="primary" @click="handleSearch"> <Button type="primary" @click="handleSearch">
<template #icon> <template #icon>
<Icon icon="ant-design:search-outlined" /> <IconifyIcon icon="ant-design:search-outlined" />
</template> </template>
搜索 搜索
</a-button> </Button>
<a-button class="ml-2" @click="handleReset"> <Button class="ml-2" @click="handleReset">
<template #icon> <template #icon>
<Icon icon="ant-design:reload-outlined" /> <IconifyIcon icon="ant-design:reload-outlined" />
</template> </template>
重置 重置
</a-button> </Button>
</a-form-item> </Form.Item>
</a-form> </Form>
</div> </div>
<Grid /> <Grid />

View File

@@ -4,7 +4,7 @@ import type { IotProductApi } from '#/api/iot/product/product';
import { ref } from 'vue'; import { ref } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { message } from 'ant-design-vue'; import { Button, Card, Descriptions, message } from 'ant-design-vue';
import { updateProductStatus } from '#/api/iot/product/product'; import { updateProductStatus } from '#/api/iot/product/product';
@@ -27,30 +27,30 @@ const router = useRouter();
const formRef = ref(); const formRef = ref();
/** 复制到剪贴板 */ /** 复制到剪贴板 */
const copyToClipboard = async (text: string) => { async function copyToClipboard(text: string) {
try { try {
await navigator.clipboard.writeText(text); await navigator.clipboard.writeText(text);
message.success('复制成功'); message.success('复制成功');
} catch { } catch {
message.error('复制失败'); message.error('复制失败');
} }
}; }
/** 跳转到设备管理 */ /** 跳转到设备管理 */
const goToDeviceList = (productId: number) => { function goToDeviceList(productId: number) {
router.push({ router.push({
path: '/iot/device/device', path: '/iot/device/device',
query: { productId: String(productId) }, query: { productId: String(productId) },
}); });
}; }
/** 打开编辑表单 */ /** 打开编辑表单 */
const openForm = (type: string, id?: number) => { function openForm(type: string, id?: number) {
formRef.value?.open(type, id); formRef.value?.open(type, id);
}; }
/** 发布产品 */ /** 发布产品 */
const confirmPublish = async (id: number) => { async function confirmPublish(id: number) {
try { try {
await updateProductStatus(id, 1); await updateProductStatus(id, 1);
message.success('发布成功'); message.success('发布成功');
@@ -58,10 +58,10 @@ const confirmPublish = async (id: number) => {
} catch { } catch {
message.error('发布失败'); message.error('发布失败');
} }
}; }
/** 撤销发布 */ /** 撤销发布 */
const confirmUnpublish = async (id: number) => { async function confirmUnpublish(id: number) {
try { try {
await updateProductStatus(id, 0); await updateProductStatus(id, 0);
message.success('撤销发布成功'); message.success('撤销发布成功');
@@ -69,7 +69,7 @@ const confirmUnpublish = async (id: number) => {
} catch { } catch {
message.error('撤销发布失败'); message.error('撤销发布失败');
} }
}; }
</script> </script>
<template> <template>
@@ -79,51 +79,51 @@ const confirmUnpublish = async (id: number) => {
<h2 class="text-xl font-bold">{{ product.name }}</h2> <h2 class="text-xl font-bold">{{ product.name }}</h2>
</div> </div>
<div class="space-x-2"> <div class="space-x-2">
<a-button <Button
:disabled="product.status === 1" :disabled="product.status === 1"
@click="openForm('update', product.id)" @click="openForm('update', product.id)"
> >
编辑 编辑
</a-button> </Button>
<a-button <Button
v-if="product.status === 0" v-if="product.status === 0"
type="primary" type="primary"
@click="confirmPublish(product.id!)" @click="confirmPublish(product.id!)"
> >
发布 发布
</a-button> </Button>
<a-button <Button
v-if="product.status === 1" v-if="product.status === 1"
danger danger
@click="confirmUnpublish(product.id!)" @click="confirmUnpublish(product.id!)"
> >
撤销发布 撤销发布
</a-button> </Button>
</div> </div>
</div> </div>
<a-card class="mt-4"> <Card class="mt-4">
<a-descriptions :column="1"> <Descriptions :column="1">
<a-descriptions-item label="ProductKey"> <Descriptions.Item label="ProductKey">
{{ product.productKey }} {{ product.productKey }}
<a-button <Button
size="small" size="small"
class="ml-2" class="ml-2"
@click="copyToClipboard(product.productKey || '')" @click="copyToClipboard(product.productKey || '')"
> >
复制 复制
</a-button> </Button>
</a-descriptions-item> </Descriptions.Item>
<a-descriptions-item label="设备总数"> <Descriptions.Item label="设备总数">
<span class="ml-5 mr-2">{{ <span class="ml-5 mr-2">
product.deviceCount ?? '加载中...' {{ product.deviceCount ?? '加载中...' }}
}}</span> </span>
<a-button size="small" @click="goToDeviceList(product.id!)"> <Button size="small" @click="goToDeviceList(product.id!)">
前往管理 前往管理
</a-button> </Button>
</a-descriptions-item> </Descriptions.Item>
</a-descriptions> </Descriptions>
</a-card> </Card>
<!-- 表单弹窗 --> <!-- 表单弹窗 -->
<ProductForm ref="formRef" @success="emit('refresh')" /> <ProductForm ref="formRef" @success="emit('refresh')" />

View File

@@ -3,6 +3,8 @@ import type { IotProductApi } from '#/api/iot/product/product';
import { DICT_TYPE } from '@vben/constants'; import { DICT_TYPE } from '@vben/constants';
import { Card, Descriptions } from 'ant-design-vue';
import { DeviceTypeEnum } from '#/api/iot/product/product'; import { DeviceTypeEnum } from '#/api/iot/product/product';
import { DictTag } from '#/components/dict-tag'; import { DictTag } from '#/components/dict-tag';
@@ -20,33 +22,33 @@ const formatDate = (date?: Date | string) => {
</script> </script>
<template> <template>
<a-card title="产品信息"> <Card title="产品信息">
<a-descriptions bordered :column="3"> <Descriptions bordered :column="3">
<a-descriptions-item label="产品名称"> <Descriptions.Item label="产品名称">
{{ product.name }} {{ product.name }}
</a-descriptions-item> </Descriptions.Item>
<a-descriptions-item label="所属分类"> <Descriptions.Item label="所属分类">
{{ product.categoryName || '-' }} {{ product.categoryName || '-' }}
</a-descriptions-item> </Descriptions.Item>
<a-descriptions-item label="设备类型"> <Descriptions.Item label="设备类型">
<DictTag <DictTag
:type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE"
:value="product.deviceType" :value="product.deviceType"
/> />
</a-descriptions-item> </Descriptions.Item>
<a-descriptions-item label="定位类型"> <Descriptions.Item label="定位类型">
{{ product.locationType ?? '-' }} {{ product.locationType ?? '-' }}
</a-descriptions-item> </Descriptions.Item>
<a-descriptions-item label="创建时间"> <Descriptions.Item label="创建时间">
{{ formatDate(product.createTime) }} {{ formatDate(product.createTime) }}
</a-descriptions-item> </Descriptions.Item>
<a-descriptions-item label="数据格式"> <Descriptions.Item label="数据格式">
{{ product.codecType || '-' }} {{ product.codecType || '-' }}
</a-descriptions-item> </Descriptions.Item>
<a-descriptions-item label="产品状态"> <Descriptions.Item label="产品状态">
<DictTag :type="DICT_TYPE.COMMON_STATUS" :value="product.status" /> <DictTag :type="DICT_TYPE.COMMON_STATUS" :value="product.status" />
</a-descriptions-item> </Descriptions.Item>
<a-descriptions-item <Descriptions.Item
v-if=" v-if="
[DeviceTypeEnum.DEVICE, DeviceTypeEnum.GATEWAY].includes( [DeviceTypeEnum.DEVICE, DeviceTypeEnum.GATEWAY].includes(
product.deviceType!, product.deviceType!,
@@ -55,10 +57,10 @@ const formatDate = (date?: Date | string) => {
label="联网方式" label="联网方式"
> >
<DictTag :type="DICT_TYPE.IOT_NET_TYPE" :value="product.netType" /> <DictTag :type="DICT_TYPE.IOT_NET_TYPE" :value="product.netType" />
</a-descriptions-item> </Descriptions.Item>
<a-descriptions-item label="产品描述" :span="3"> <Descriptions.Item label="产品描述" :span="3">
{{ product.description || '-' }} {{ product.description || '-' }}
</a-descriptions-item> </Descriptions.Item>
</a-descriptions> </Descriptions>
</a-card> </Card>
</template> </template>

View File

@@ -6,7 +6,7 @@ import { useRoute, useRouter } from 'vue-router';
import { Page } from '@vben/common-ui'; import { Page } from '@vben/common-ui';
import { message } from 'ant-design-vue'; import { message, Tabs } from 'ant-design-vue';
import { getDeviceCount } from '#/api/iot/device/device'; import { getDeviceCount } from '#/api/iot/device/device';
import { getProduct } from '#/api/iot/product/product'; import { getProduct } from '#/api/iot/product/product';
@@ -29,7 +29,7 @@ const activeTab = ref('info');
provide('product', product); provide('product', product);
/** 获取产品详情 */ /** 获取产品详情 */
const getProductData = async (productId: number) => { async function getProductData(productId: number) {
loading.value = true; loading.value = true;
try { try {
product.value = await getProduct(productId); product.value = await getProduct(productId);
@@ -38,10 +38,10 @@ const getProductData = async (productId: number) => {
} finally { } finally {
loading.value = false; loading.value = false;
} }
}; }
/** 查询设备数量 */ /** 查询设备数量 */
const getDeviceCountData = async (productId: number) => { async function getDeviceCountData(productId: number) {
try { try {
return await getDeviceCount(productId); return await getDeviceCount(productId);
} catch (error) { } catch (error) {
@@ -53,7 +53,7 @@ const getDeviceCountData = async (productId: number) => {
); );
return 0; return 0;
} }
}; }
/** 初始化 */ /** 初始化 */
onMounted(async () => { onMounted(async () => {
@@ -86,16 +86,16 @@ onMounted(async () => {
@refresh="() => getProductData(id)" @refresh="() => getProductData(id)"
/> />
<a-tabs v-model:active-key="activeTab" class="mt-4"> <Tabs v-model:active-key="activeTab" class="mt-4">
<a-tab-pane key="info" tab="产品信息"> <Tabs.TabPane key="info" tab="产品信息">
<ProductDetailsInfo v-if="activeTab === 'info'" :product="product" /> <ProductDetailsInfo v-if="activeTab === 'info'" :product="product" />
</a-tab-pane> </Tabs.TabPane>
<a-tab-pane key="thingModel" tab="物模型(功能定义)"> <Tabs.TabPane key="thingModel" tab="物模型(功能定义)">
<IoTProductThingModel <IoTProductThingModel
v-if="activeTab === 'thingModel'" v-if="activeTab === 'thingModel'"
:product-id="id" :product-id="id"
/> />
</a-tab-pane> </Tabs.TabPane>
</a-tabs> </Tabs>
</Page> </Page>
</template> </template>

View File

@@ -1,7 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'; import { computed, onMounted, reactive, ref } from 'vue';
import { Button, Form, FormItem, Select, Table } from 'ant-design-vue'; import { IconifyIcon } from '@vben/icons';
import { Button, Form, Select, Table } from 'ant-design-vue';
import { getSimpleDeviceList } from '#/api/iot/device/device'; import { getSimpleDeviceList } from '#/api/iot/device/device';
import { getSimpleProductList } from '#/api/iot/product/product'; import { getSimpleProductList } from '#/api/iot/product/product';
@@ -31,23 +33,23 @@ const upstreamMethods = computed(() => {
}); });
/** 根据产品 ID 过滤设备 */ /** 根据产品 ID 过滤设备 */
const getFilteredDevices = (productId: number) => { function getFilteredDevices(productId: number) {
if (!productId) return []; if (!productId) return [];
return deviceList.value.filter( return deviceList.value.filter(
(device: any) => device.productId === productId, (device: any) => device.productId === productId,
); );
}; }
/** 判断是否需要显示标识符选择器 */ /** 判断是否需要显示标识符选择器 */
const shouldShowIdentifierSelect = (row: any) => { function shouldShowIdentifierSelect(row: any) {
return [ return [
IotDeviceMessageMethodEnum.EVENT_POST.method, IotDeviceMessageMethodEnum.EVENT_POST.method,
IotDeviceMessageMethodEnum.PROPERTY_POST.method, IotDeviceMessageMethodEnum.PROPERTY_POST.method,
].includes(row.method); ].includes(row.method);
}; }
/** 获取物模型选项 */ /** 获取物模型选项 */
const getThingModelOptions = (row: any) => { function getThingModelOptions(row: any) {
if (!row.productId || !shouldShowIdentifierSelect(row)) { if (!row.productId || !shouldShowIdentifierSelect(row)) {
return []; return [];
} }
@@ -66,28 +68,28 @@ const getThingModelOptions = (row: any) => {
label: `${item.name} (${item.identifier})`, label: `${item.name} (${item.identifier})`,
value: item.identifier, value: item.identifier,
})); }));
}; }
/** 加载产品列表 */ /** 加载产品列表 */
const loadProductList = async () => { async function loadProductList() {
try { try {
productList.value = await getSimpleProductList(); productList.value = await getSimpleProductList();
} catch (error) { } catch (error) {
console.error('加载产品列表失败:', error); console.error('加载产品列表失败:', error);
} }
}; }
/** 加载设备列表 */ /** 加载设备列表 */
const loadDeviceList = async () => { async function loadDeviceList() {
try { try {
deviceList.value = await getSimpleDeviceList(); deviceList.value = await getSimpleDeviceList();
} catch (error) { } catch (error) {
console.error('加载设备列表失败:', error); console.error('加载设备列表失败:', error);
} }
}; }
/** 加载物模型数据 */ /** 加载物模型数据 */
const loadThingModel = async (productId: number) => { async function loadThingModel(productId: number) {
// 已缓存,无需重复加载 // 已缓存,无需重复加载
if (thingModelCache.value.has(productId)) { if (thingModelCache.value.has(productId)) {
return; return;
@@ -98,18 +100,18 @@ const loadThingModel = async (productId: number) => {
} catch (error) { } catch (error) {
console.error('加载物模型失败:', error); console.error('加载物模型失败:', error);
} }
}; }
/** 产品变化时处理 */ /** 产品变化时处理 */
const handleProductChange = async (row: any, _index: number) => { async function handleProductChange(row: any, _index: number) {
row.deviceId = 0; row.deviceId = 0;
row.method = undefined; row.method = undefined;
row.identifier = undefined; row.identifier = undefined;
row.identifierLoading = false; row.identifierLoading = false;
}; }
/** 消息方法变化时处理 */ /** 消息方法变化时处理 */
const handleMethodChange = async (row: any, _index: number) => { async function handleMethodChange(row: any, _index: number) {
// 清空标识符 // 清空标识符
row.identifier = undefined; row.identifier = undefined;
// 如果需要加载物模型数据 // 如果需要加载物模型数据
@@ -118,10 +120,10 @@ const handleMethodChange = async (row: any, _index: number) => {
await loadThingModel(row.productId); await loadThingModel(row.productId);
row.identifierLoading = false; row.identifierLoading = false;
} }
}; }
/** 新增按钮操作 */ /** 新增按钮操作 */
const handleAdd = () => { function handleAdd() {
const row = { const row = {
productId: undefined, productId: undefined,
deviceId: undefined, deviceId: undefined,
@@ -130,25 +132,25 @@ const handleAdd = () => {
identifierLoading: false, identifierLoading: false,
}; };
formData.value.push(row); formData.value.push(row);
}; }
/** 删除按钮操作 */ /** 删除按钮操作 */
const handleDelete = (index: number) => { function handleDelete(index: number) {
formData.value.splice(index, 1); formData.value.splice(index, 1);
}; }
/** 表单校验 */ /** 表单校验 */
const validate = () => { function validate() {
return formRef.value.validate(); return formRef.value.validate();
}; }
/** 表单值 */ /** 表单值 */
const getData = () => { function getData() {
return formData.value; return formData.value;
}; }
/** 设置表单值 */ /** 设置表单值 */
const setData = (data: any[]) => { function setData(data: any[]) {
// 确保每个项都有必要的字段 // 确保每个项都有必要的字段
formData.value = (data || []).map((item) => ({ formData.value = (data || []).map((item) => ({
...item, ...item,
@@ -160,7 +162,7 @@ const setData = (data: any[]) => {
await loadThingModel(item.productId); await loadThingModel(item.productId);
} }
}); });
}; }
/** 初始化 */ /** 初始化 */
onMounted(async () => { onMounted(async () => {
@@ -209,7 +211,7 @@ defineExpose({ validate, getData, setData });
> >
<template #bodyCell="{ column, record, index }"> <template #bodyCell="{ column, record, index }">
<template v-if="column.dataIndex === 'productId'"> <template v-if="column.dataIndex === 'productId'">
<FormItem <Form.Item
:name="['data', index, 'productId']" :name="['data', index, 'productId']"
:rules="formRules.productId" :rules="formRules.productId"
class="mb-0" class="mb-0"
@@ -227,10 +229,10 @@ defineExpose({ validate, getData, setData });
" "
@change="() => handleProductChange(record, index)" @change="() => handleProductChange(record, index)"
/> />
</FormItem> </Form.Item>
</template> </template>
<template v-else-if="column.dataIndex === 'deviceId'"> <template v-else-if="column.dataIndex === 'deviceId'">
<FormItem <Form.Item
:name="['data', index, 'deviceId']" :name="['data', index, 'deviceId']"
:rules="formRules.deviceId" :rules="formRules.deviceId"
class="mb-0" class="mb-0"
@@ -251,10 +253,10 @@ defineExpose({ validate, getData, setData });
})), })),
]" ]"
/> />
</FormItem> </Form.Item>
</template> </template>
<template v-else-if="column.dataIndex === 'method'"> <template v-else-if="column.dataIndex === 'method'">
<FormItem <Form.Item
:name="['data', index, 'method']" :name="['data', index, 'method']"
:rules="formRules.method" :rules="formRules.method"
class="mb-0" class="mb-0"
@@ -275,10 +277,10 @@ defineExpose({ validate, getData, setData });
" "
@change="() => handleMethodChange(record, index)" @change="() => handleMethodChange(record, index)"
/> />
</FormItem> </Form.Item>
</template> </template>
<template v-else-if="column.dataIndex === 'identifier'"> <template v-else-if="column.dataIndex === 'identifier'">
<FormItem :name="['data', index, 'identifier']" class="mb-0"> <Form.Item :name="['data', index, 'identifier']" class="mb-0">
<Select <Select
v-if="shouldShowIdentifierSelect(record)" v-if="shouldShowIdentifierSelect(record)"
v-model:value="record.identifier" v-model:value="record.identifier"
@@ -291,7 +293,7 @@ defineExpose({ validate, getData, setData });
" "
:options="getThingModelOptions(record)" :options="getThingModelOptions(record)"
/> />
</FormItem> </Form.Item>
</template> </template>
<template v-else-if="column.title === '操作'"> <template v-else-if="column.title === '操作'">
<Button type="link" danger @click="handleDelete(index)">删除</Button> <Button type="link" danger @click="handleDelete(index)">删除</Button>
@@ -300,7 +302,7 @@ defineExpose({ validate, getData, setData });
</Table> </Table>
<div class="mt-3 text-center"> <div class="mt-3 text-center">
<Button type="primary" @click="handleAdd"> <Button type="primary" @click="handleAdd">
<Icon icon="ant-design:plus-outlined" class="mr-1" /> <IconifyIcon icon="ant-design:plus-outlined" class="mr-1" />
添加数据源 添加数据源
</Button> </Button>
</div> </div>

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref } from 'vue'; import { computed, ref, watch } from 'vue';
import { useVbenModal } from '@vben/common-ui'; import { useVbenModal } from '@vben/common-ui';
@@ -10,7 +10,7 @@ import {
createDataSink, createDataSink,
getDataSink, getDataSink,
updateDataSink, updateDataSink,
} from '#/api/iot/rule/data'; } from '#/api/iot/rule/data/sink';
import { $t } from '#/locales'; import { $t } from '#/locales';
import { import {
@@ -106,7 +106,7 @@ const [Modal, modalApi] = useVbenModal({
// 监听类型变化,重置配置 // 监听类型变化,重置配置
watch( watch(
() => formApi.form.type, () => formApi.getValues().then((values) => values.type),
(newType) => { (newType) => {
if (formData.value && newType !== formData.value.type) { if (formData.value && newType !== formData.value.type) {
formData.value.config = {}; formData.value.config = {};

View File

@@ -1,4 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, onMounted, ref, watch } from 'vue';
import { isEmpty } from '@vben/utils'; import { isEmpty } from '@vben/utils';
import { useVModel } from '@vueuse/core'; import { useVModel } from '@vueuse/core';
@@ -12,7 +14,7 @@ const props = defineProps<{
modelValue: any; modelValue: any;
}>(); }>();
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(['update:modelValue']);
const config = useVModel(props, 'modelValue', emit) as Ref<any>; const config = useVModel(props, 'modelValue', emit) as any;
// noinspection HttpUrlsUsage // noinspection HttpUrlsUsage
/** URL处理 */ /** URL处理 */

View File

@@ -1,4 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted } from 'vue';
import { isEmpty } from '@vben/utils'; import { isEmpty } from '@vben/utils';
import { useVModel } from '@vueuse/core'; import { useVModel } from '@vueuse/core';
@@ -10,7 +12,7 @@ const props = defineProps<{
modelValue: any; modelValue: any;
}>(); }>();
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(['update:modelValue']);
const config = useVModel(props, 'modelValue', emit) as Ref<any>; const config = useVModel(props, 'modelValue', emit) as any;
/** 组件初始化 */ /** 组件初始化 */
onMounted(() => { onMounted(() => {

View File

@@ -1,4 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted } from 'vue';
import { isEmpty } from '@vben/utils'; import { isEmpty } from '@vben/utils';
import { useVModel } from '@vueuse/core'; import { useVModel } from '@vueuse/core';
@@ -10,7 +12,7 @@ const props = defineProps<{
modelValue: any; modelValue: any;
}>(); }>();
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(['update:modelValue']);
const config = useVModel(props, 'modelValue', emit) as Ref<any>; const config = useVModel(props, 'modelValue', emit) as any;
/** 组件初始化 */ /** 组件初始化 */
onMounted(() => { onMounted(() => {

View File

@@ -1,4 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted } from 'vue';
import { isEmpty } from '@vben/utils'; import { isEmpty } from '@vben/utils';
import { useVModel } from '@vueuse/core'; import { useVModel } from '@vueuse/core';
@@ -10,7 +12,7 @@ const props = defineProps<{
modelValue: any; modelValue: any;
}>(); }>();
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(['update:modelValue']);
const config = useVModel(props, 'modelValue', emit) as Ref<any>; const config = useVModel(props, 'modelValue', emit) as any;
/** 组件初始化 */ /** 组件初始化 */
onMounted(() => { onMounted(() => {

View File

@@ -1,4 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted } from 'vue';
import { isEmpty } from '@vben/utils'; import { isEmpty } from '@vben/utils';
import { useVModel } from '@vueuse/core'; import { useVModel } from '@vueuse/core';
@@ -10,7 +12,7 @@ const props = defineProps<{
modelValue: any; modelValue: any;
}>(); }>();
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(['update:modelValue']);
const config = useVModel(props, 'modelValue', emit) as Ref<any>; const config = useVModel(props, 'modelValue', emit) as any;
/** 组件初始化 */ /** 组件初始化 */
onMounted(() => { onMounted(() => {

View File

@@ -1,4 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted } from 'vue';
import { isEmpty } from '@vben/utils'; import { isEmpty } from '@vben/utils';
import { useVModel } from '@vueuse/core'; import { useVModel } from '@vueuse/core';
@@ -10,7 +12,7 @@ const props = defineProps<{
modelValue: any; modelValue: any;
}>(); }>();
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(['update:modelValue']);
const config = useVModel(props, 'modelValue', emit) as Ref<any>; const config = useVModel(props, 'modelValue', emit) as any;
/** 组件初始化 */ /** 组件初始化 */
onMounted(() => { onMounted(() => {

View File

@@ -1,7 +1,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, watch } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { isEmpty } from '@vben/utils'; import { isEmpty } from '@vben/utils';
import { Delete, Plus } from '@element-plus/icons-vue'; import { Button, Input } from 'ant-design-vue';
defineOptions({ name: 'KeyValueEditor' }); defineOptions({ name: 'KeyValueEditor' });
@@ -20,19 +23,19 @@ interface KeyValueItem {
const items = ref<KeyValueItem[]>([]); // 内部 key-value 项列表 const items = ref<KeyValueItem[]>([]); // 内部 key-value 项列表
/** 添加项目 */ /** 添加项目 */
const addItem = () => { function addItem() {
items.value.push({ key: '', value: '' }); items.value.push({ key: '', value: '' });
updateModelValue(); updateModelValue();
}; }
/** 移除项目 */ /** 移除项目 */
const removeItem = (index: number) => { function removeItem(index: number) {
items.value.splice(index, 1); items.value.splice(index, 1);
updateModelValue(); updateModelValue();
}; }
/** 更新 modelValue */ /** 更新 modelValue */
const updateModelValue = () => { function updateModelValue() {
const result: Record<string, string> = {}; const result: Record<string, string> = {};
items.value.forEach((item) => { items.value.forEach((item) => {
if (item.key) { if (item.key) {
@@ -40,7 +43,7 @@ const updateModelValue = () => {
} }
}); });
emit('update:modelValue', result); emit('update:modelValue', result);
}; }
/** 监听项目变化 */ /** 监听项目变化 */
watch(items, updateModelValue, { deep: true }); watch(items, updateModelValue, { deep: true });
@@ -61,19 +64,15 @@ watch(
<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="index" class="mb-2 flex w-full">
<el-input v-model="item.key" class="mr-2" placeholder="键" /> <Input v-model="item.key" class="mr-2" placeholder="键" />
<el-input v-model="item.value" placeholder="值" /> <Input v-model="item.value" placeholder="值" />
<el-button class="ml-2" text type="danger" @click="removeItem(index)"> <Button class="ml-2" text danger @click="removeItem(index)">
<el-icon> <IconifyIcon icon="ant-design:delete-outlined" />
<Delete />
</el-icon>
删除 删除
</el-button> </Button>
</div> </div>
<el-button text type="primary" @click="addItem"> <Button text type="primary" @click="addItem">
<el-icon> <IconifyIcon icon="ant-design:plus-outlined" />
<Plus />
</el-icon>
{{ addButtonText }} {{ addButtonText }}
</el-button> </Button>
</template> </template>

View File

@@ -1,8 +1,11 @@
<!-- 告警配置组件 --> <!-- 告警配置组件 -->
<script setup lang="ts"> <script setup lang="ts">
import { useVModel } from '@vueuse/core'; import { onMounted, ref } from 'vue';
import { AlertConfigApi } from '#/api/iot/alert/config'; import { useVModel } from '@vueuse/core';
import { Form, Select, Tag } from 'ant-design-vue';
import { getAlertConfigPage } from '#/api/iot/alert/config';
/** 告警配置组件 */ /** 告警配置组件 */
defineOptions({ name: 'AlertConfig' }); defineOptions({ name: 'AlertConfig' });
@@ -24,17 +27,17 @@ const alertConfigs = ref<any[]>([]); // 告警配置列表
* 处理选择变化事件 * 处理选择变化事件
* @param value 选中的值 * @param value 选中的值
*/ */
const handleChange = (value?: number) => { function handleChange(value?: any) {
emit('update:modelValue', value); emit('update:modelValue', value);
}; }
/** /**
* 加载告警配置列表 * 加载告警配置列表
*/ */
const loadAlertConfigs = async () => { async function loadAlertConfigs() {
loading.value = true; loading.value = true;
try { try {
const data = await AlertConfigApi.getAlertConfigPage({ const data = await getAlertConfigPage({
pageNo: 1, pageNo: 1,
pageSize: 100, pageSize: 100,
enabled: true, // 只加载启用的配置 enabled: true, // 只加载启用的配置
@@ -43,7 +46,7 @@ const loadAlertConfigs = async () => {
} finally { } finally {
loading.value = false; loading.value = false;
} }
}; }
// 组件挂载时加载数据 // 组件挂载时加载数据
onMounted(() => { onMounted(() => {
@@ -53,8 +56,8 @@ onMounted(() => {
<template> <template>
<div class="w-full"> <div class="w-full">
<el-form-item label="告警配置" required> <Form.Item label="告警配置" required>
<el-select <Select
v-model="localValue" v-model="localValue"
placeholder="请选择告警配置" placeholder="请选择告警配置"
filterable filterable
@@ -63,7 +66,7 @@ onMounted(() => {
class="w-full" class="w-full"
:loading="loading" :loading="loading"
> >
<el-option <Select.Option
v-for="config in alertConfigs" v-for="config in alertConfigs"
:key="config.id" :key="config.id"
:label="config.name" :label="config.name"
@@ -71,12 +74,12 @@ onMounted(() => {
> >
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<span>{{ config.name }}</span> <span>{{ config.name }}</span>
<el-tag :type="config.enabled ? 'success' : 'danger'" size="small"> <Tag :type="config.enabled ? 'success' : 'danger'" size="small">
{{ config.enabled ? '启用' : '禁用' }} {{ config.enabled ? '启用' : '禁用' }}
</el-tag> </Tag>
</div> </div>
</el-option> </Select.Option>
</el-select> </Select>
</el-form-item> </Form.Item>
</div> </div>
</template> </template>

View File

@@ -2,7 +2,10 @@
<script setup lang="ts"> <script setup lang="ts">
import type { TriggerCondition } from '#/api/iot/rule/scene'; import type { TriggerCondition } from '#/api/iot/rule/scene';
import { computed, ref } from 'vue';
import { useVModel } from '@vueuse/core'; import { useVModel } from '@vueuse/core';
import { Col, Form, Row, Select } from 'ant-design-vue';
import { import {
getConditionTypeOptions, getConditionTypeOptions,
@@ -61,9 +64,9 @@ const propertyConfig = ref<any>(null); // 属性配置
const isDeviceCondition = computed(() => { const isDeviceCondition = computed(() => {
return ( return (
condition.value.type === condition.value.type ===
IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS || IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS.toString() ||
condition.value.type === condition.value.type ===
IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY.toString()
); );
}); // 计算属性:判断是否为设备相关条件 }); // 计算属性:判断是否为设备相关条件
@@ -72,25 +75,25 @@ const isDeviceCondition = computed(() => {
* @param field 字段名 * @param field 字段名
* @param value 字段值 * @param value 字段值
*/ */
const updateConditionField = (field: any, value: any) => { function updateConditionField(field: any, value: any) {
(condition.value as any)[field] = value; (condition.value as any)[field] = value;
emit('update:modelValue', condition.value); emit('update:modelValue', condition.value);
}; }
/** /**
* 更新整个条件对象 * 更新整个条件对象
* @param newCondition 新的条件对象 * @param newCondition 新的条件对象
*/ */
const updateCondition = (newCondition: TriggerCondition) => { function updateCondition(newCondition: TriggerCondition) {
condition.value = newCondition; condition.value = newCondition;
emit('update:modelValue', condition.value); emit('update:modelValue', condition.value);
}; }
/** /**
* 处理条件类型变化事件 * 处理条件类型变化事件
* @param type 条件类型 * @param type 条件类型
*/ */
const handleConditionTypeChange = (type: number) => { function handleConditionTypeChange(type: any) {
// 根据条件类型清理字段 // 根据条件类型清理字段
const isCurrentTime = const isCurrentTime =
type === IotRuleSceneTriggerConditionTypeEnum.CURRENT_TIME; type === IotRuleSceneTriggerConditionTypeEnum.CURRENT_TIME;
@@ -115,26 +118,26 @@ const handleConditionTypeChange = (type: number) => {
// 清空参数值 // 清空参数值
condition.value.param = ''; condition.value.param = '';
}; }
/** 处理产品变化事件 */ /** 处理产品变化事件 */
const handleProductChange = (_: number) => { function handleProductChange(_: any) {
// 产品变化时清空设备和属性 // 产品变化时清空设备和属性
condition.value.deviceId = undefined; condition.value.deviceId = undefined;
condition.value.identifier = ''; condition.value.identifier = '';
}; }
/** 处理设备变化事件 */ /** 处理设备变化事件 */
const handleDeviceChange = (_: number) => { function handleDeviceChange(_: any) {
// 设备变化时清空属性 // 设备变化时清空属性
condition.value.identifier = ''; condition.value.identifier = '';
}; }
/** /**
* 处理属性变化事件 * 处理属性变化事件
* @param propertyInfo 属性信息对象 * @param propertyInfo 属性信息对象
*/ */
const handlePropertyChange = (propertyInfo: { config: any; type: string }) => { function handlePropertyChange(propertyInfo: { config: any; type: string }) {
propertyType.value = propertyInfo.type; propertyType.value = propertyInfo.type;
propertyConfig.value = propertyInfo.config; propertyConfig.value = propertyInfo.config;
@@ -142,43 +145,45 @@ const handlePropertyChange = (propertyInfo: { config: any; type: string }) => {
condition.value.operator = condition.value.operator =
IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value; IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value;
condition.value.param = ''; condition.value.param = '';
}; }
/** 处理操作符变化事件 */ /** 处理操作符变化事件 */
const handleOperatorChange = () => { function handleOperatorChange() {
// 重置值 // 重置值
condition.value.param = ''; condition.value.param = '';
}; }
</script> </script>
<template> <template>
<div class="gap-16px flex flex-col"> <div class="gap-16px flex flex-col">
<!-- 条件类型选择 --> <!-- 条件类型选择 -->
<el-row :gutter="16"> <Row :gutter="16">
<el-col :span="8"> <Col :span="8">
<el-form-item label="条件类型" required> <Form.Item label="条件类型" required>
<el-select <Select
:model-value="condition.type" :model-value="condition.type"
@update:model-value="(value) => updateConditionField('type', value)" @update:model-value="
(value: any) => updateConditionField('type', value)
"
@change="handleConditionTypeChange" @change="handleConditionTypeChange"
placeholder="请选择条件类型" placeholder="请选择条件类型"
class="w-full" class="w-full"
> >
<el-option <Select.Option
v-for="option in getConditionTypeOptions()" v-for="option in getConditionTypeOptions()"
:key="option.value" :key="option.value"
:label="option.label" :label="option.label"
:value="option.value" :value="option.value"
/> />
</el-select> </Select>
</el-form-item> </Form.Item>
</el-col> </Col>
</el-row> </Row>
<!-- 产品设备选择 - 设备相关条件的公共部分 --> <!-- 产品设备选择 - 设备相关条件的公共部分 -->
<el-row v-if="isDeviceCondition" :gutter="16"> <Row v-if="isDeviceCondition" :gutter="16">
<el-col :span="12"> <Col :span="12">
<el-form-item label="产品" required> <Form.Item label="产品" required>
<ProductSelector <ProductSelector
:model-value="condition.productId" :model-value="condition.productId"
@update:model-value=" @update:model-value="
@@ -186,10 +191,10 @@ const handleOperatorChange = () => {
" "
@change="handleProductChange" @change="handleProductChange"
/> />
</el-form-item> </Form.Item>
</el-col> </Col>
<el-col :span="12"> <Col :span="12">
<el-form-item label="设备" required> <Form.Item label="设备" required>
<DeviceSelector <DeviceSelector
:model-value="condition.deviceId" :model-value="condition.deviceId"
@update:model-value=" @update:model-value="
@@ -198,9 +203,9 @@ const handleOperatorChange = () => {
:product-id="condition.productId" :product-id="condition.productId"
@change="handleDeviceChange" @change="handleDeviceChange"
/> />
</el-form-item> </Form.Item>
</el-col> </Col>
</el-row> </Row>
<!-- 设备状态条件配置 --> <!-- 设备状态条件配置 -->
<div <div
@@ -210,11 +215,11 @@ const handleOperatorChange = () => {
class="gap-16px flex flex-col" class="gap-16px flex flex-col"
> >
<!-- 状态和操作符选择 --> <!-- 状态和操作符选择 -->
<el-row :gutter="16"> <Row :gutter="16">
<!-- 操作符选择 --> <!-- 操作符选择 -->
<el-col :span="12"> <Col :span="12">
<el-form-item label="操作符" required> <Form.Item label="操作符" required>
<el-select <Select
:model-value="condition.operator" :model-value="condition.operator"
@update:model-value=" @update:model-value="
(value) => updateConditionField('operator', value) (value) => updateConditionField('operator', value)
@@ -222,51 +227,52 @@ const handleOperatorChange = () => {
placeholder="请选择操作符" placeholder="请选择操作符"
class="w-full" class="w-full"
> >
<el-option <Select.Option
v-for="option in statusOperatorOptions" v-for="option in statusOperatorOptions"
:key="option.value" :key="option.value"
:label="option.label" :label="option.label"
:value="option.value" :value="option.value"
/> />
</el-select> </Select>
</el-form-item> </Form.Item>
</el-col> </Col>
<!-- 状态选择 --> <!-- 状态选择 -->
<el-col :span="12"> <Col :span="12">
<el-form-item label="设备状态" required> <Form.Item label="设备状态" required>
<el-select <Select
:model-value="condition.param" :model-value="condition.param"
@update:model-value=" @update:model-value="
(value) => updateConditionField('param', value) (value: any) => updateConditionField('param', value)
" "
placeholder="请选择设备状态" placeholder="请选择设备状态"
class="w-full" class="w-full"
> >
<el-option <Select.Option
v-for="option in deviceStatusOptions" v-for="option in deviceStatusOptions"
:key="option.value" :key="option.value"
:label="option.label" :label="option.label"
:value="option.value" :value="option.value"
/> />
</el-select> </Select>
</el-form-item> </Form.Item>
</el-col> </Col>
</el-row> </Row>
</div> </div>
<!-- 设备属性条件配置 --> <!-- 设备属性条件配置 -->
<div <div
v-else-if=" v-else-if="
condition.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY condition.type ===
IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY.toString()
" "
class="space-y-16px" class="space-y-16px"
> >
<!-- 属性配置 --> <!-- 属性配置 -->
<el-row :gutter="16"> <Row :gutter="16">
<!-- 属性/事件/服务选择 --> <!-- 属性/事件/服务选择 -->
<el-col :span="6"> <Col :span="6">
<el-form-item label="监控项" required> <Form.Item label="监控项" required>
<PropertySelector <PropertySelector
:model-value="condition.identifier" :model-value="condition.identifier"
@update:model-value=" @update:model-value="
@@ -277,12 +283,12 @@ const handleOperatorChange = () => {
:device-id="condition.deviceId" :device-id="condition.deviceId"
@change="handlePropertyChange" @change="handlePropertyChange"
/> />
</el-form-item> </Form.Item>
</el-col> </Col>
<!-- 操作符选择 --> <!-- 操作符选择 -->
<el-col :span="6"> <Col :span="6">
<el-form-item label="操作符" required> <Form.Item label="操作符" required>
<OperatorSelector <OperatorSelector
:model-value="condition.operator" :model-value="condition.operator"
@update:model-value=" @update:model-value="
@@ -291,12 +297,12 @@ const handleOperatorChange = () => {
:property-type="propertyType" :property-type="propertyType"
@change="handleOperatorChange" @change="handleOperatorChange"
/> />
</el-form-item> </Form.Item>
</el-col> </Col>
<!-- 值输入 --> <!-- 值输入 -->
<el-col :span="12"> <Col :span="12">
<el-form-item label="比较值" required> <Form.Item label="比较值" required>
<ValueInput <ValueInput
:model-value="condition.param" :model-value="condition.param"
@update:model-value=" @update:model-value="
@@ -306,15 +312,16 @@ const handleOperatorChange = () => {
:operator="condition.operator" :operator="condition.operator"
:property-config="propertyConfig" :property-config="propertyConfig"
/> />
</el-form-item> </Form.Item>
</el-col> </Col>
</el-row> </Row>
</div> </div>
<!-- 当前时间条件配置 --> <!-- 当前时间条件配置 -->
<CurrentTimeConditionConfig <CurrentTimeConditionConfig
v-else-if=" v-else-if="
condition.type === IotRuleSceneTriggerConditionTypeEnum.CURRENT_TIME condition.type ===
IotRuleSceneTriggerConditionTypeEnum.CURRENT_TIME.toString()
" "
:model-value="condition" :model-value="condition"
@update:model-value="updateCondition" @update:model-value="updateCondition"
@@ -323,7 +330,7 @@ const handleOperatorChange = () => {
</template> </template>
<style scoped> <style scoped>
:deep(.el-form-item) { :deep(.ant-form-item) {
margin-bottom: 0; margin-bottom: 0;
} }
</style> </style>

View File

@@ -2,6 +2,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { TriggerCondition } from '#/api/iot/rule/scene'; import type { TriggerCondition } from '#/api/iot/rule/scene';
import { computed } from 'vue';
import { useVModel } from '@vueuse/core'; import { useVModel } from '@vueuse/core';
import { IotRuleSceneTriggerTimeOperatorEnum } from '#/views/iot/utils/constants'; import { IotRuleSceneTriggerTimeOperatorEnum } from '#/views/iot/utils/constants';