This commit is contained in:
YunaiV
2025-10-11 12:52:46 +08:00
32 changed files with 482 additions and 513 deletions

View File

@@ -18,23 +18,23 @@
width: 100%;
min-height: 36px;
.el-button {
.ant-button {
text-align: center;
}
.el-button-group {
.ant-button-group {
margin: 4px;
}
.el-tooltip__popper {
.el-button {
.ant-tooltip__popper {
.ant-button {
width: 100%;
padding-right: 8px;
padding-left: 8px;
text-align: left;
}
.el-button:hover {
.ant-button:hover {
color: #fff;
background: rgb(64 158 255 / 80%);
}
@@ -175,7 +175,6 @@ pre {
}
.hljs {
word-break: break-word;
white-space: pre-wrap;
}

View File

@@ -59,7 +59,7 @@
line-height: 32px;
}
.el-form-item {
.ant-form-item {
width: 100%;
padding-bottom: 18px;
margin-bottom: 0;
@@ -106,22 +106,22 @@
margin-top: 8px;
}
.element-drawer__button > .el-button {
.element-drawer__button > .ant-button {
width: 100%;
}
.el-collapse-item__content {
.ant-collapse-item__content {
padding-bottom: 0;
}
.el-input.is-disabled .el-input__inner {
.ant-input.is-disabled .ant-input__inner {
color: #999;
}
.el-form-item.el-form-item--mini {
.ant-form-item.ant-form-item--mini {
margin-bottom: 0;
& + .el-form-item {
& + .ant-form-item {
margin-top: 16px;
}
}

View File

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

View File

@@ -102,7 +102,7 @@ export function useIotHome() {
}
/** 格式化数字 - 大数字显示为 K/M */
export const formatNumber = (num: number): string => {
export function formatNumber(num: number): string {
if (num >= 1_000_000) {
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.toString();
};
}

View File

@@ -1,12 +1,7 @@
import type { VbenFormSchema } from '#/adapter/form';
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 { $t } from '#/locales';
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>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { IoTOtaFirmwareApi } from '#/api/iot/ota/firmware';
import { Page, useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteOtaFirmware, getOtaFirmwarePage } from '#/api/iot/ota/firmware';
import { $t } from '#/locales';
import Form from '../modules/OtaFirmwareForm.vue';
import {
handleDeleteFirmware,
useGridFormSchema,
useGridOptions,
} from './data';
import { useGridColumns, useGridFormSchema } from './data';
defineOptions({ name: 'IoTOtaFirmware' });
@@ -35,21 +35,56 @@ function handleEdit(row: IoTOtaFirmwareApi.Firmware) {
formModalApi.setData({ type: 'update', id: row.id }).open();
}
/** 删除固件 */
async function handleDelete(row: IoTOtaFirmwareApi.Firmware) {
await handleDeleteFirmware(row, onRefresh);
}
/** 查看固件详情 */
function handleDetail(row: IoTOtaFirmwareApi.Firmware) {
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({
formOptions: {
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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,19 +1,10 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { IotProductCategoryApi } from '#/api/iot/product/category';
import { DICT_TYPE } from '@vben/constants';
import { handleTree } from '@vben/utils';
import { message } from 'ant-design-vue';
import { z } from '#/adapter/form';
import {
deleteProductCategory,
getProductCategoryPage,
getSimpleProductCategoryList,
} from '#/api/iot/product/category';
import { $t } from '#/locales';
import { getSimpleProductCategoryList } from '#/api/iot/product/category';
/** 新增/修改产品分类的表单 */
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 { 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 {
deleteProductCategory,
getProductCategoryPage,
} from '#/api/iot/product/category';
import { $t } from '#/locales';
import {
handleDeleteCategory,
queryProductCategoryList,
useGridColumns,
useGridFormSchema,
} from './data';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/ProductCategoryForm.vue';
defineOptions({ name: 'IoTProductCategory' });
@@ -39,7 +41,17 @@ function handleEdit(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({
@@ -57,7 +69,18 @@ const [Grid, gridApi] = useVbenVxeGrid({
},
proxyConfig: {
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: {

View File

@@ -5,17 +5,10 @@ import { ref } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { downloadFileFromBlobPart } from '@vben/utils';
import { message } from 'ant-design-vue';
import { z } from '#/adapter/form';
import { getSimpleProductCategoryList } from '#/api/iot/product/category';
import {
deleteProduct,
exportProduct,
getProductPage,
} from '#/api/iot/product/product';
import { getProductPage } from '#/api/iot/product/product';
/** 新增/修改产品的表单 */
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) {
return await getProductPage({

View File

@@ -6,21 +6,20 @@ import { useRouter } from 'vue-router';
import { Page, useVbenModal } from '@vben/common-ui';
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 {
deleteProduct,
exportProduct,
getProductPage,
} from '#/api/crm/product';
import { getSimpleProductCategoryList } from '#/api/iot/product/category';
import { $t } from '#/locales';
import {
getCategoryName,
handleDeleteProduct,
handleExportProduct,
loadCategoryList,
queryProductList,
useGridColumns,
useImagePreview,
} from './data';
import { useGridColumns, useImagePreview } from './data';
// @ts-ignore
import ProductCardView from './modules/ProductCardView.vue';
import ProductForm from './modules/ProductForm.vue';
@@ -47,14 +46,15 @@ const [FormModal, formModalApi] = useVbenModal({
});
// 加载产品分类列表
const loadCategories = async () => {
categoryList.value = await loadCategoryList();
};
async function loadCategories() {
categoryList.value = await getSimpleProductCategoryList();
}
// 获取分类名称
const getCategoryNameByValue = (categoryId: number) => {
return getCategoryName(categoryList.value, categoryId);
};
function getCategoryNameByValue(categoryId: number) {
const category = categoryList.value.find((c: any) => c.id === categoryId);
return category?.name || '未分类';
}
/** 搜索 */
function handleSearch() {
@@ -84,7 +84,8 @@ function handleRefresh() {
/** 导出表格 */
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) {
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({
@@ -129,7 +140,13 @@ const [Grid, gridApi] = useVbenVxeGrid({
keepSource: true,
proxyConfig: {
ajax: {
query: ({ page }) => queryProductList({ page }, searchParams.value),
query: async ({ page }) => {
return await getProductPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...searchParams.value,
});
},
},
},
rowConfig: {
@@ -336,6 +353,6 @@ onMounted(() => {
}
.ant-image-preview-operations {
background: rgba(0, 0, 0, 0.7) !important;
background: rgb(0 0 0 / 70%) !important;
}
</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);
return category?.name || '未分类';
};
}
// 获取产品列表
const getList = async () => {
async function getList() {
loading.value = true;
try {
const data = await getProductPage({
@@ -66,23 +66,23 @@ const getList = async () => {
} finally {
loading.value = false;
}
};
}
// 处理页码变化
const handlePageChange = (page: number, pageSize: number) => {
function handlePageChange(page: number, pageSize: number) {
queryParams.value.pageNo = page;
queryParams.value.pageSize = pageSize;
getList();
};
}
// 获取设备类型颜色
const getDeviceTypeColor = (deviceType: number) => {
function getDeviceTypeColor(deviceType: number) {
const colors: Record<number, string> = {
0: 'blue',
1: 'green',
};
return colors[deviceType] || 'default';
};
}
onMounted(() => {
getList();
@@ -131,9 +131,9 @@ defineExpose({
<div class="info-list flex-1">
<div class="info-item">
<span class="info-label">产品分类</span>
<span class="info-value text-primary">{{
getCategoryName(item.categoryId)
}}</span>
<span class="info-value text-primary">
{{ getCategoryName(item.categoryId) }}
</span>
</div>
<div class="info-item">
<span class="info-label">产品类型</span>
@@ -152,9 +152,9 @@ defineExpose({
<div class="info-item">
<span class="info-label">产品标识</span>
<Tooltip :title="item.productKey || item.id" placement="top">
<span class="info-value product-key">{{
item.productKey || item.id
}}</span>
<span class="info-value product-key">
{{ item.productKey || item.id }}
</span>
</Tooltip>
</div>
</div>
@@ -236,44 +236,44 @@ defineExpose({
.product-card-view {
.product-card {
height: 100%;
transition: all 0.3s ease;
overflow: hidden;
border: 1px solid #e8e8e8;
border-radius: 8px;
overflow: hidden;
transition: all 0.3s ease;
&:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
border-color: #d9d9d9;
box-shadow: 0 4px 16px rgb(0 0 0 / 8%);
transform: translateY(-2px);
}
:deep(.ant-card-body) {
height: 100%;
display: flex;
flex-direction: column;
height: 100%;
}
// 产品图标
.product-icon {
width: 48px;
height: 48px;
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
color: white;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 8px;
color: white;
flex-shrink: 0;
}
// 产品标题
.product-title {
font-size: 16px;
font-weight: 600;
color: #1f2937;
line-height: 1.5;
overflow: hidden;
text-overflow: ellipsis;
font-size: 16px;
font-weight: 600;
line-height: 1.5;
color: #1f2937;
white-space: nowrap;
}
@@ -290,16 +290,16 @@ defineExpose({
}
.info-label {
color: #6b7280;
margin-right: 8px;
flex-shrink: 0;
margin-right: 8px;
color: #6b7280;
}
.info-value {
color: #1f2937;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
font-weight: 500;
color: #1f2937;
white-space: nowrap;
&.text-primary {
@@ -308,15 +308,15 @@ defineExpose({
}
.product-key {
font-family: 'Courier New', monospace;
font-size: 12px;
color: #374151;
display: inline-block;
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-family: 'Courier New', monospace;
font-size: 12px;
vertical-align: middle;
color: #374151;
white-space: nowrap;
cursor: help;
}
@@ -328,15 +328,15 @@ defineExpose({
// 3D 图标
.product-3d-icon {
width: 100px;
height: 100px;
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 100px;
height: 100px;
color: #667eea;
background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%);
border-radius: 8px;
flex-shrink: 0;
color: #667eea;
}
// 按钮组
@@ -344,8 +344,8 @@ defineExpose({
display: flex;
gap: 8px;
padding-top: 12px;
border-top: 1px solid #f0f0f0;
margin-top: auto;
border-top: 1px solid #f0f0f0;
.action-btn {
flex: 1;
@@ -359,8 +359,8 @@ defineExpose({
border-color: #1890ff;
&:hover {
background: #1890ff;
color: white;
background: #1890ff;
}
}
@@ -369,8 +369,8 @@ defineExpose({
border-color: #52c41a;
&:hover {
background: #52c41a;
color: white;
background: #52c41a;
}
}
@@ -379,8 +379,8 @@ defineExpose({
border-color: #722ed1;
&:hover {
background: #722ed1;
color: white;
background: #722ed1;
}
}

View File

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

View File

@@ -4,7 +4,7 @@ import type { IotProductApi } from '#/api/iot/product/product';
import { ref } from 'vue';
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';
@@ -27,30 +27,30 @@ const router = useRouter();
const formRef = ref();
/** 复制到剪贴板 */
const copyToClipboard = async (text: string) => {
async function copyToClipboard(text: string) {
try {
await navigator.clipboard.writeText(text);
message.success('复制成功');
} catch {
message.error('复制失败');
}
};
}
/** 跳转到设备管理 */
const goToDeviceList = (productId: number) => {
function goToDeviceList(productId: number) {
router.push({
path: '/iot/device/device',
query: { productId: String(productId) },
});
};
}
/** 打开编辑表单 */
const openForm = (type: string, id?: number) => {
function openForm(type: string, id?: number) {
formRef.value?.open(type, id);
};
}
/** 发布产品 */
const confirmPublish = async (id: number) => {
async function confirmPublish(id: number) {
try {
await updateProductStatus(id, 1);
message.success('发布成功');
@@ -58,10 +58,10 @@ const confirmPublish = async (id: number) => {
} catch {
message.error('发布失败');
}
};
}
/** 撤销发布 */
const confirmUnpublish = async (id: number) => {
async function confirmUnpublish(id: number) {
try {
await updateProductStatus(id, 0);
message.success('撤销发布成功');
@@ -69,7 +69,7 @@ const confirmUnpublish = async (id: number) => {
} catch {
message.error('撤销发布失败');
}
};
}
</script>
<template>
@@ -79,51 +79,51 @@ const confirmUnpublish = async (id: number) => {
<h2 class="text-xl font-bold">{{ product.name }}</h2>
</div>
<div class="space-x-2">
<a-button
<Button
:disabled="product.status === 1"
@click="openForm('update', product.id)"
>
编辑
</a-button>
<a-button
</Button>
<Button
v-if="product.status === 0"
type="primary"
@click="confirmPublish(product.id!)"
>
发布
</a-button>
<a-button
</Button>
<Button
v-if="product.status === 1"
danger
@click="confirmUnpublish(product.id!)"
>
撤销发布
</a-button>
</Button>
</div>
</div>
<a-card class="mt-4">
<a-descriptions :column="1">
<a-descriptions-item label="ProductKey">
<Card class="mt-4">
<Descriptions :column="1">
<Descriptions.Item label="ProductKey">
{{ product.productKey }}
<a-button
<Button
size="small"
class="ml-2"
@click="copyToClipboard(product.productKey || '')"
>
复制
</a-button>
</a-descriptions-item>
<a-descriptions-item label="设备总数">
<span class="ml-5 mr-2">{{
product.deviceCount ?? '加载中...'
}}</span>
<a-button size="small" @click="goToDeviceList(product.id!)">
</Button>
</Descriptions.Item>
<Descriptions.Item label="设备总数">
<span class="ml-5 mr-2">
{{ product.deviceCount ?? '加载中...' }}
</span>
<Button size="small" @click="goToDeviceList(product.id!)">
前往管理
</a-button>
</a-descriptions-item>
</a-descriptions>
</a-card>
</Button>
</Descriptions.Item>
</Descriptions>
</Card>
<!-- 表单弹窗 -->
<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 { Card, Descriptions } from 'ant-design-vue';
import { DeviceTypeEnum } from '#/api/iot/product/product';
import { DictTag } from '#/components/dict-tag';
@@ -20,33 +22,33 @@ const formatDate = (date?: Date | string) => {
</script>
<template>
<a-card title="产品信息">
<a-descriptions bordered :column="3">
<a-descriptions-item label="产品名称">
<Card title="产品信息">
<Descriptions bordered :column="3">
<Descriptions.Item label="产品名称">
{{ product.name }}
</a-descriptions-item>
<a-descriptions-item label="所属分类">
</Descriptions.Item>
<Descriptions.Item label="所属分类">
{{ product.categoryName || '-' }}
</a-descriptions-item>
<a-descriptions-item label="设备类型">
</Descriptions.Item>
<Descriptions.Item label="设备类型">
<DictTag
:type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE"
:value="product.deviceType"
/>
</a-descriptions-item>
<a-descriptions-item label="定位类型">
</Descriptions.Item>
<Descriptions.Item label="定位类型">
{{ product.locationType ?? '-' }}
</a-descriptions-item>
<a-descriptions-item label="创建时间">
</Descriptions.Item>
<Descriptions.Item label="创建时间">
{{ formatDate(product.createTime) }}
</a-descriptions-item>
<a-descriptions-item label="数据格式">
</Descriptions.Item>
<Descriptions.Item label="数据格式">
{{ product.codecType || '-' }}
</a-descriptions-item>
<a-descriptions-item label="产品状态">
</Descriptions.Item>
<Descriptions.Item label="产品状态">
<DictTag :type="DICT_TYPE.COMMON_STATUS" :value="product.status" />
</a-descriptions-item>
<a-descriptions-item
</Descriptions.Item>
<Descriptions.Item
v-if="
[DeviceTypeEnum.DEVICE, DeviceTypeEnum.GATEWAY].includes(
product.deviceType!,
@@ -55,10 +57,10 @@ const formatDate = (date?: Date | string) => {
label="联网方式"
>
<DictTag :type="DICT_TYPE.IOT_NET_TYPE" :value="product.netType" />
</a-descriptions-item>
<a-descriptions-item label="产品描述" :span="3">
</Descriptions.Item>
<Descriptions.Item label="产品描述" :span="3">
{{ product.description || '-' }}
</a-descriptions-item>
</a-descriptions>
</a-card>
</Descriptions.Item>
</Descriptions>
</Card>
</template>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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