!315 refactor:【antd】【iot】设备管理跟后端对齐,必要的 ReqVO、RespVO,子设备管理实现

Merge pull request !315 from haohaoMT/dev
This commit is contained in:
芋道源码
2026-01-05 12:10:39 +00:00
committed by Gitee
16 changed files with 537 additions and 141 deletions

View File

@@ -3,39 +3,76 @@ import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace IotDeviceApi {
// TODO @haohao需要跟后端对齐必要的 ReqVO、RespVO
/** 设备 */
export interface Device {
id?: number; // 设备 ID主键自增
/** 设备新增/修改 Request VO */
export interface DeviceSaveReqVO {
id?: number; // 设备编号
deviceName: string; // 设备名称
nickname?: string; // 备注名称
serialNumber?: string; // 设备序列号
picUrl?: string; // 设备图片
groupIds?: number[]; // 设备分组编号数组
productId: number; // 产品编号(必填)
gatewayId?: number; // 网关设备 ID
config?: string; // 设备配置
locationType: number; // 定位类型(必填)
latitude?: number; // 设备位置的纬度
longitude?: number; // 设备位置的经度
}
/** 设备更新分组 Request VO */
export interface DeviceUpdateGroupReqVO {
ids: number[]; // 设备编号列表(必填)
groupIds: number[]; // 分组编号列表(必填)
}
/** 设备分页 Request VO */
export interface DevicePageReqVO extends PageParam {
deviceName?: string; // 设备名称
nickname?: string; // 备注名称
productId?: number; // 产品编号
deviceType?: number; // 设备类型
status?: number; // 设备状态
groupId?: number; // 设备分组编号
gatewayId?: number; // 网关设备 ID
}
/** 设备 Response VO */
export interface DeviceRespVO {
id: number; // 设备编号
deviceName: string; // 设备名称
nickname?: string; // 设备备注名称
serialNumber?: string; // 设备序列号
picUrl?: string; // 设备图片
groupIds?: number[]; // 设备分组编号数组
productId: number; // 产品编号
productKey?: string; // 产品标识
deviceType?: number; // 设备类型
nickname?: string; // 设备备注名称
gatewayId?: number; // 网关设备 ID
state?: number; // 设备状态
status?: number; // 设备状态(兼容字段)
onlineTime?: Date; // 最后上线时间
offlineTime?: Date; // 最后离线时间
activeTime?: Date; // 设备激活时间
createTime?: Date; // 创建时间
ip?: string; // 设备的 IP 地址
firmwareVersion?: string; // 设备的固件版本
deviceSecret?: string; // 设备密钥,用于设备认证,需安全存储
mqttClientId?: string; // MQTT 客户端 ID
mqttUsername?: string; // MQTT 用户名
mqttPassword?: string; // MQTT 密码
authType?: string; // 认证类型
locationType?: number; // 定位类型
deviceSecret?: string; // 设备密钥,用于设备认证
authType?: string; // 认证类型(如一机一密、动态注册)
config?: string; // 设备配置
locationType?: number; // 定位方式
latitude?: number; // 设备位置的纬度
longitude?: number; // 设备位置的经度
areaId?: number; // 地区编码
address?: string; // 设备详细地址
serialNumber?: string; // 设备序列号
config?: string; // 设备配置
groupIds?: number[]; // 添加分组 ID
picUrl?: string; // 设备图片
location?: string; // 位置信息(格式:经度,纬度)
createTime?: Date; // 创建时间
}
/** 设备认证信息 Response VO */
export interface DeviceAuthInfoRespVO {
clientId: string; // 客户端 ID
username: string; // 用户名
password: string; // 密码
}
/** 设备导入 Response VO */
export interface DeviceImportRespVO {
createDeviceNames?: string[]; // 创建成功的设备名称列表
updateDeviceNames?: string[]; // 更新成功的设备名称列表
failureDeviceNames?: Record<string, string>; // 失败的设备名称及原因
}
/** IoT 设备属性详细 VO */
@@ -56,30 +93,17 @@ export namespace IotDeviceApi {
updateTime: Date; // 更新时间
}
/** 设备认证参数 VO */
export interface DeviceAuthInfo {
clientId: string; // 客户端 ID
username: string; // 用户名
password: string; // 密码
}
/** 设备发送消息 Request VO */
export interface DeviceMessageSendReq {
deviceId: number; // 设备编号
method: string; // 请求方法
params?: any; // 请求参数
}
/** 设备分组更新请求 */
export interface DeviceGroupUpdateReq {
ids: number[]; // 设备 ID 列表
groupIds: number[]; // 分组 ID 列表
}
}
/** 查询设备分页 */
export function getDevicePage(params: PageParam) {
return requestClient.get<PageResult<IotDeviceApi.Device>>(
export function getDevicePage(params: IotDeviceApi.DevicePageReqVO) {
return requestClient.get<PageResult<IotDeviceApi.DeviceRespVO>>(
'/iot/device/page',
{ params },
);
@@ -87,38 +111,40 @@ export function getDevicePage(params: PageParam) {
/** 查询设备详情 */
export function getDevice(id: number) {
return requestClient.get<IotDeviceApi.Device>(`/iot/device/get?id=${id}`);
return requestClient.get<IotDeviceApi.DeviceRespVO>(
`/iot/device/get?id=${id}`,
);
}
/** 新增设备 */
export function createDevice(data: IotDeviceApi.Device) {
return requestClient.post('/iot/device/create', data);
export function createDevice(data: IotDeviceApi.DeviceSaveReqVO) {
return requestClient.post<number>('/iot/device/create', data);
}
/** 修改设备 */
export function updateDevice(data: IotDeviceApi.Device) {
return requestClient.put('/iot/device/update', data);
export function updateDevice(data: IotDeviceApi.DeviceSaveReqVO) {
return requestClient.put<boolean>('/iot/device/update', data);
}
/** 修改设备分组 */
export function updateDeviceGroup(data: IotDeviceApi.DeviceGroupUpdateReq) {
return requestClient.put('/iot/device/update-group', data);
export function updateDeviceGroup(data: IotDeviceApi.DeviceUpdateGroupReqVO) {
return requestClient.put<boolean>('/iot/device/update-group', data);
}
/** 删除单个设备 */
export function deleteDevice(id: number) {
return requestClient.delete(`/iot/device/delete?id=${id}`);
return requestClient.delete<boolean>(`/iot/device/delete?id=${id}`);
}
/** 删除多个设备 */
export function deleteDeviceList(ids: number[]) {
return requestClient.delete('/iot/device/delete-list', {
return requestClient.delete<boolean>('/iot/device/delete-list', {
params: { ids: ids.join(',') },
});
}
/** 导出设备 */
export function exportDeviceExcel(params: any) {
export function exportDeviceExcel(params: IotDeviceApi.DevicePageReqVO) {
return requestClient.download('/iot/device/export-excel', { params });
}
@@ -129,16 +155,22 @@ export function getDeviceCount(productId: number) {
/** 获取设备的精简信息列表 */
export function getSimpleDeviceList(deviceType?: number, productId?: number) {
return requestClient.get<IotDeviceApi.Device[]>('/iot/device/simple-list', {
params: { deviceType, productId },
});
return requestClient.get<IotDeviceApi.DeviceRespVO[]>(
'/iot/device/simple-list',
{
params: { deviceType, productId },
},
);
}
/** 根据产品编号,获取设备的精简信息列表 */
export function getDeviceListByProductId(productId: number) {
return requestClient.get<IotDeviceApi.Device[]>('/iot/device/simple-list', {
params: { productId },
});
return requestClient.get<IotDeviceApi.DeviceRespVO[]>(
'/iot/device/simple-list',
{
params: { productId },
},
);
}
/** 获取导入模板 */
@@ -148,10 +180,13 @@ export function importDeviceTemplate() {
/** 导入设备 */
export function importDevice(file: File, updateSupport: boolean) {
return requestClient.upload('/iot/device/import', {
file,
updateSupport,
});
return requestClient.upload<IotDeviceApi.DeviceImportRespVO>(
'/iot/device/import',
{
file,
updateSupport,
},
);
}
/** 获取设备属性最新数据 */
@@ -172,7 +207,7 @@ export function getHistoryDevicePropertyList(params: any) {
/** 获取设备认证信息 */
export function getDeviceAuthInfo(id: number) {
return requestClient.get<IotDeviceApi.DeviceAuthInfo>(
return requestClient.get<IotDeviceApi.DeviceAuthInfoRespVO>(
'/iot/device/get-auth-info',
{ params: { id } },
);

View File

@@ -9,8 +9,8 @@ import { getSimpleDeviceList } from '#/api/iot/device/device';
import { getSimpleDeviceGroupList } from '#/api/iot/device/group';
import { getSimpleProductList } from '#/api/iot/product/product';
/** 新增/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
/** 基础表单字段 */
export function useBasicFormSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
@@ -36,6 +36,14 @@ export function useFormSchema(): VbenFormSchema[] {
},
rules: 'required',
},
{
component: 'Input',
fieldName: 'deviceType',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'deviceName',
label: 'DeviceName',
@@ -62,7 +70,7 @@ export function useFormSchema(): VbenFormSchema[] {
component: 'ApiSelect',
componentProps: {
api: () => getSimpleDeviceList(DeviceTypeEnum.GATEWAY),
labelField: 'nickname',
labelField: 'deviceName',
valueField: 'id',
placeholder: '子设备可选择父设备',
},
@@ -71,6 +79,12 @@ export function useFormSchema(): VbenFormSchema[] {
show: (values) => values.deviceType === DeviceTypeEnum.GATEWAY_SUB,
},
},
];
}
/** 高级设置表单字段(更多设置) */
export function useAdvancedFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'nickname',
label: '备注名称',
@@ -89,6 +103,11 @@ export function useFormSchema(): VbenFormSchema[] {
.optional()
.or(z.literal('')),
},
{
fieldName: 'picUrl',
label: '设备图片',
component: 'ImageUpload',
},
{
fieldName: 'groupIds',
label: '设备分组',
@@ -278,6 +297,14 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
title: '备注名称',
minWidth: 120,
},
{
field: 'picUrl',
title: '设备图片',
width: 100,
cellRender: {
name: 'CellImage',
},
},
{
field: 'productId',
title: '所属产品',

View File

@@ -31,7 +31,7 @@ const router = useRouter();
const id = Number(route.params.id);
const loading = ref(true);
const product = ref<IotProductApi.Product>({} as IotProductApi.Product);
const device = ref<IotDeviceApi.Device>({} as IotDeviceApi.Device);
const device = ref<IotDeviceApi.DeviceRespVO>({} as IotDeviceApi.DeviceRespVO);
const activeTab = ref('info');
const thingModelList = ref<ThingModelData[]>([]);

View File

@@ -12,7 +12,7 @@ import { IotDeviceMessageMethodEnum } from '#/views/iot/utils/constants';
defineOptions({ name: 'DeviceDetailConfig' });
const props = defineProps<{
device: IotDeviceApi.Device;
device: IotDeviceApi.DeviceRespVO;
}>();
const emit = defineEmits<{
@@ -114,7 +114,7 @@ async function updateDeviceConfig() {
await updateDevice({
id: props.device.id,
config: JSON.stringify(config.value),
} as IotDeviceApi.Device);
} as IotDeviceApi.DeviceSaveReqVO);
message.success({ content: '更新成功!' });
// 触发 success 事件
emit('success');

View File

@@ -12,7 +12,7 @@ import DeviceForm from '../../modules/form.vue';
interface Props {
product: IotProductApi.Product;
device: IotDeviceApi.Device;
device: IotDeviceApi.DeviceRespVO;
loading?: boolean;
}
@@ -50,7 +50,7 @@ function goToProductDetail(productId: number | undefined) {
}
/** 打开编辑表单 */
function openEditForm(row: IotDeviceApi.Device) {
function openEditForm(row: IotDeviceApi.DeviceRespVO) {
formModalApi.setData(row).open();
}
</script>

View File

@@ -24,7 +24,7 @@ import { getDeviceAuthInfo } from '#/api/iot/device/device';
import { DictTag } from '#/components/dict-tag';
interface Props {
device: IotDeviceApi.Device;
device: IotDeviceApi.DeviceRespVO;
product: IotProductApi.Product;
}
@@ -32,8 +32,8 @@ const props = defineProps<Props>();
const authDialogVisible = ref(false);
const authPasswordVisible = ref(false);
const authInfo = ref<IotDeviceApi.DeviceAuthInfo>(
{} as IotDeviceApi.DeviceAuthInfo,
const authInfo = ref<IotDeviceApi.DeviceAuthInfoRespVO>(
{} as IotDeviceApi.DeviceAuthInfoRespVO,
);
/** 控制地图显示的标志 */
@@ -75,8 +75,7 @@ function handleAuthInfoDialogClose() {
<Card class="h-full">
<template #title>
<div class="flex items-center">
<!-- TODO @haohao图标尽量使用中立的这样 ep 版本呢好迁移 -->
<IconifyIcon class="mr-2 text-primary" icon="ep:info-filled" />
<IconifyIcon class="mr-2 text-primary" icon="lucide:info" />
<span>设备信息</span>
</div>
</template>
@@ -142,8 +141,7 @@ function handleAuthInfoDialogClose() {
<template #title>
<div class="flex items-center justify-between">
<div class="flex items-center">
<!-- TODO @haohao图标尽量使用中立的这样 ep 版本呢好迁移 -->
<IconifyIcon class="mr-2 text-primary" icon="ep:location" />
<IconifyIcon class="mr-2 text-primary" icon="lucide:map-pin" />
<span>设备位置</span>
</div>
<div class="text-sm text-gray-500">
@@ -162,8 +160,7 @@ function handleAuthInfoDialogClose() {
v-else
class="flex h-full w-full items-center justify-center rounded bg-gray-50 text-gray-400"
>
<!-- TODO @haohao图标尽量使用中立的这样 ep 版本呢好迁移 -->
<IconifyIcon class="mr-2" icon="ep:warning" />
<IconifyIcon class="mr-2" icon="lucide:alert-triangle" />
<span>暂无位置信息</span>
</div>
</div>

View File

@@ -34,7 +34,7 @@ import DataDefinition from '../../../../thingmodel/modules/components/data-defin
import DeviceDetailsMessage from './message.vue';
const props = defineProps<{
device: IotDeviceApi.Device;
device: IotDeviceApi.DeviceRespVO;
product: IotProductApi.Product;
thingModelList: ThingModelData[];
}>();

View File

@@ -1,45 +1,223 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { IotDeviceApi } from '#/api/iot/device/device';
import type { IotProductApi } from '#/api/iot/product/product';
import { Card, Empty } from 'ant-design-vue';
import { onMounted, reactive, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
// TODO @haohao这里要实现一把么
import { Page } from '@vben/common-ui';
import { DeviceTypeEnum, DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { IconifyIcon } from '@vben/icons';
import { Button, Input, Select, Space } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getDevicePage } from '#/api/iot/device/device';
import { getSimpleProductList } from '#/api/iot/product/product';
interface Props {
deviceId: number;
}
defineProps<Props>();
const props = defineProps<Props>();
const router = useRouter();
const loading = ref(false);
const subDevices = ref<any[]>([]);
/** 产品列表 */
const products = ref<IotProductApi.Product[]>([]);
/** 获取子设备列表 */
async function getSubDeviceList() {
loading.value = true;
try {
// TODO: 实现获取子设备列表的API调用
// const data = await getSubDevicesByGatewayId(deviceId);
// subDevices.value = data || [];
subDevices.value = [];
} catch (error) {
console.error('获取子设备列表失败:', error);
} finally {
loading.value = false;
}
/** 查询参数 */
const queryParams = reactive({
deviceName: '',
status: undefined as number | undefined,
});
/** Grid 列定义 */
function useGridColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'deviceName',
title: 'DeviceName',
minWidth: 150,
},
{
field: 'nickname',
title: '备注名称',
minWidth: 120,
},
{
field: 'productId',
title: '所属产品',
minWidth: 120,
slots: { default: 'product' },
},
{
field: 'state',
title: '设备状态',
minWidth: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.IOT_DEVICE_STATE },
},
},
{
field: 'onlineTime',
title: '最后上线时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
title: '操作',
width: 150,
fixed: 'right',
slots: { default: 'actions' },
},
];
}
onMounted(() => {
getSubDeviceList();
/** 创建 Grid 实例 */
const [Grid, gridApi] = useVbenVxeGrid<IotDeviceApi.DeviceRespVO>({
gridOptions: {
columns: useGridColumns(),
height: 'auto',
rowConfig: {
keyField: 'id',
isHover: true,
},
proxyConfig: {
ajax: {
query: async ({
page,
}: {
page: { currentPage: number; pageSize: number };
}) => {
if (!props.deviceId) {
return { list: [], total: 0 };
}
return await getDevicePage({
pageNo: page.currentPage,
pageSize: page.pageSize,
gatewayId: props.deviceId,
deviceType: DeviceTypeEnum.GATEWAY_SUB,
deviceName: queryParams.deviceName || undefined,
status: queryParams.status,
} as IotDeviceApi.DevicePageReqVO);
},
},
},
toolbarConfig: {
refresh: true,
search: false,
},
pagerConfig: {
enabled: true,
},
},
});
/** 搜索操作 */
function handleQuery() {
gridApi.query();
}
/** 重置搜索 */
function resetQuery() {
queryParams.deviceName = '';
queryParams.status = undefined;
handleQuery();
}
/** 获取产品名称 */
function getProductName(productId: number) {
const product = products.value.find((p) => p.id === productId);
return product?.name || '-';
}
/** 查看详情 */
function openDetail(id: number) {
router.push({ name: 'IoTDeviceDetail', params: { id } });
}
/** 监听设备ID变化 */
watch(
() => props.deviceId,
(newValue) => {
if (newValue) {
handleQuery();
}
},
);
/** 初始化 */
onMounted(async () => {
// 获取产品列表
products.value = await getSimpleProductList();
// 如果设备ID存在则查询列表
if (props.deviceId) {
handleQuery();
}
});
</script>
<template>
<Card :loading="loading" title="子设备管理">
<Empty
:image="Empty.PRESENTED_IMAGE_SIMPLE"
description="暂无子设备数据,此功能待实现"
/>
<!-- TODO: 实现子设备列表展示和管理功能 -->
</Card>
<Page auto-content-height>
<!-- 搜索区域 -->
<div class="mb-4 flex flex-wrap items-center gap-3">
<Input
v-model:value="queryParams.deviceName"
placeholder="请输入设备名称"
style="width: 200px"
allow-clear
@press-enter="handleQuery"
/>
<Select
v-model:value="queryParams.status"
allow-clear
placeholder="请选择设备状态"
style="width: 160px"
>
<Select.Option
v-for="dict in getDictOptions(DICT_TYPE.IOT_DEVICE_STATE, 'number')"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</Select.Option>
</Select>
<Space>
<Button type="primary" @click="handleQuery">
<IconifyIcon icon="ep:search" class="mr-5px" />
搜索
</Button>
<Button @click="resetQuery">
<IconifyIcon icon="ep:refresh-right" class="mr-5px" />
重置
</Button>
</Space>
</div>
<!-- 子设备列表 -->
<Grid>
<!-- 所属产品列 -->
<template #product="{ row }">
{{ getProductName(row.productId) }}
</template>
<!-- 操作列 -->
<template #actions="{ row }">
<TableAction
:actions="[
{
label: '查看详情',
type: 'link',
icon: ACTION_ICON.VIEW,
onClick: openDetail.bind(null, row.id!),
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@@ -1,5 +1,4 @@
<script setup lang="ts">
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { IotDeviceApi } from '#/api/iot/device/device';
import type { IotDeviceGroupApi } from '#/api/iot/device/group';
import type { IotProductApi } from '#/api/iot/product/product';
@@ -51,6 +50,9 @@ const viewMode = ref<'card' | 'list'>('card');
const cardViewRef = ref();
const checkedIds = ref<number[]>([]);
/** 判断是否为列表视图 */
const isListView = () => viewMode.value === 'list';
const [DeviceFormModal, deviceFormModalApi] = useVbenModal({
connectedComponent: DeviceForm,
destroyOnClose: true,
@@ -66,13 +68,13 @@ const [DeviceImportFormModal, deviceImportFormModalApi] = useVbenModal({
destroyOnClose: true,
});
const queryParams = ref({
const queryParams = ref<Partial<IotDeviceApi.DevicePageReqVO>>({
deviceName: '',
nickname: '',
productId: undefined as number | undefined,
deviceType: undefined as number | undefined,
status: undefined as number | undefined,
groupId: undefined as number | undefined,
productId: undefined,
deviceType: undefined,
status: undefined,
groupId: undefined,
}); // 搜索参数
/** 搜索 */
@@ -112,7 +114,11 @@ async function handleViewModeChange(mode: 'card' | 'list') {
/** 导出表格 */
async function handleExport() {
const data = await exportDeviceExcel(queryParams.value);
const data = await exportDeviceExcel({
...queryParams.value,
pageNo: 1,
pageSize: 999999,
} as IotDeviceApi.DevicePageReqVO);
downloadFileFromBlobPart({ fileName: '物联网设备.xls', source: data });
}
@@ -141,12 +147,12 @@ function handleCreate() {
}
/** 编辑设备 */
function handleEdit(row: IotDeviceApi.Device) {
function handleEdit(row: IotDeviceApi.DeviceRespVO) {
deviceFormModalApi.setData(row).open();
}
/** 删除设备 */
async function handleDelete(row: IotDeviceApi.Device) {
async function handleDelete(row: IotDeviceApi.DeviceRespVO) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.deviceName]),
duration: 0,
@@ -197,12 +203,12 @@ function handleImport() {
function handleRowCheckboxChange({
records,
}: {
records: IotDeviceApi.Device[];
records: IotDeviceApi.DeviceRespVO[];
}) {
checkedIds.value = records.map((item) => item.id!);
}
const [Grid, gridApi] = useVbenVxeGrid({
const [Grid, gridApi] = useVbenVxeGrid<IotDeviceApi.DeviceRespVO>({
gridOptions: {
checkboxConfig: {
highlight: true,
@@ -213,12 +219,16 @@ const [Grid, gridApi] = useVbenVxeGrid({
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }) => {
query: async ({
page,
}: {
page: { currentPage: number; pageSize: number };
}) => {
return await getDevicePage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...queryParams.value,
});
} as IotDeviceApi.DevicePageReqVO);
},
},
},
@@ -230,7 +240,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
refresh: true,
search: true,
},
} as VxeTableGridOptions<IotDeviceApi.Device>,
},
gridEvents: {
checkboxAll: handleRowCheckboxChange,
checkboxChange: handleRowCheckboxChange,
@@ -388,7 +398,7 @@ onMounted(async () => {
type: 'primary',
icon: 'ant-design:folder-add-outlined',
auth: ['iot:device:update'],
ifShow: () => viewMode === 'list',
ifShow: isListView,
disabled: isEmpty(checkedIds),
onClick: handleAddToGroup,
},
@@ -398,7 +408,7 @@ onMounted(async () => {
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['iot:device:delete'],
ifShow: () => viewMode === 'list',
ifShow: isListView,
disabled: isEmpty(checkedIds),
onClick: handleDeleteBatch,
},
@@ -490,7 +500,14 @@ onMounted(async () => {
ref="cardViewRef"
:products="products"
:device-groups="deviceGroups"
:search-params="queryParams"
:search-params="{
deviceName: queryParams.deviceName || '',
nickname: queryParams.nickname || '',
productId: queryParams.productId,
deviceType: queryParams.deviceType,
status: queryParams.status,
groupId: queryParams.groupId,
}"
@create="handleCreate"
@edit="handleEdit"
@delete="handleDelete"

View File

@@ -1,4 +1,6 @@
<script lang="ts" setup>
import type { IotDeviceApi } from '#/api/iot/device/device';
import { onMounted, ref } from 'vue';
import { DICT_TYPE } from '@vben/constants';
@@ -9,6 +11,7 @@ import {
Card,
Col,
Empty,
Image,
Pagination,
Popconfirm,
Row,
@@ -43,9 +46,9 @@ const emit = defineEmits<{
}>();
const loading = ref(false);
const list = ref<any[]>([]);
const list = ref<IotDeviceApi.DeviceRespVO[]>([]);
const total = ref(0);
const queryParams = ref({
const queryParams = ref<Partial<IotDeviceApi.DevicePageReqVO>>({
pageNo: 1,
pageSize: 12,
});
@@ -63,7 +66,7 @@ async function getList() {
const data = await getDevicePage({
...queryParams.value,
...props.searchParams,
});
} as IotDeviceApi.DevicePageReqVO);
list.value = data.list || [];
total.value = data.total || 0;
} finally {
@@ -128,8 +131,8 @@ onMounted(() => {
/>
</div>
<!-- 内容区域 -->
<div class="mb-3">
<div class="info-list">
<div class="mb-3 flex items-start">
<div class="info-list flex-1">
<div class="info-item">
<span class="info-label">所属产品</span>
<a
@@ -154,13 +157,27 @@ onMounted(() => {
</div>
<div class="info-item">
<span class="info-label">Deviceid</span>
<Tooltip :title="item.Deviceid || item.id" placement="top">
<Tooltip :title="String(item.id)" placement="top">
<span class="info-value device-id cursor-pointer">
{{ item.Deviceid || item.id }}
{{ item.id }}
</span>
</Tooltip>
</div>
</div>
<!-- 设备图片 -->
<div class="device-image">
<Image
v-if="item.picUrl"
:src="item.picUrl"
:preview="true"
class="size-full rounded object-cover"
/>
<IconifyIcon
v-else
icon="lucide:image"
class="text-2xl opacity-50"
/>
</div>
</div>
<!-- 按钮组 -->
<div class="action-buttons">
@@ -263,6 +280,19 @@ onMounted(() => {
font-size: 12px;
}
// 设备图片
.device-image {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 80px;
height: 80px;
color: #1890ff;
background: linear-gradient(135deg, #40a9ff15 0%, #1890ff15 100%);
border-radius: 8px;
}
// 信息列表
.info-list {
.info-item {
@@ -385,6 +415,11 @@ html.dark {
color: rgb(255 255 255 / 75%);
}
}
.device-image {
color: #69c0ff;
background: linear-gradient(135deg, #40a9ff25 0%, #1890ff25 100%);
}
}
}
}

View File

@@ -1,22 +1,27 @@
<script setup lang="ts">
import type { IotDeviceApi } from '#/api/iot/device/device';
import type { IotProductApi } from '#/api/iot/product/product';
import { computed, ref } from 'vue';
import { computed, nextTick, onMounted, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { Collapse, message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createDevice, getDevice, updateDevice } from '#/api/iot/device/device';
import { getSimpleProductList } from '#/api/iot/product/product';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
import { useAdvancedFormSchema, useBasicFormSchema } from '../data';
defineOptions({ name: 'IoTDeviceForm' });
const emit = defineEmits(['success']);
const formData = ref<IotDeviceApi.Device>();
const formData = ref<IotDeviceApi.DeviceRespVO>();
const products = ref<IotProductApi.Product[]>([]);
const activeKey = ref<string[]>([]);
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['设备'])
@@ -31,10 +36,54 @@ const [Form, formApi] = useVbenForm({
},
wrapperClass: 'grid-cols-1',
layout: 'horizontal',
schema: useFormSchema(),
schema: useBasicFormSchema(),
showDefaultActions: false,
handleValuesChange: async (values, changedFields) => {
// 当产品 ProductId 变化时,自动设置设备类型
if (changedFields.includes('productId')) {
const productId = values.productId;
if (!productId) {
await formApi.setFieldValue('deviceType', undefined);
return;
}
// 从产品列表中查找产品
const product = products.value.find((p) => p.id === productId);
if (product?.deviceType !== undefined) {
await formApi.setFieldValue('deviceType', product.deviceType);
}
}
},
});
const [AdvancedForm, advancedFormApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
},
wrapperClass: 'grid-cols-1',
layout: 'horizontal',
schema: useAdvancedFormSchema(),
showDefaultActions: false,
});
/** 获取高级表单的值(如果表单未挂载,则从 formData 中获取) */
async function getAdvancedFormValues() {
if (advancedFormApi.isMounted) {
return await advancedFormApi.getValues();
}
// 表单未挂载(折叠状态),从 formData 中获取
return {
nickname: formData.value?.nickname,
picUrl: formData.value?.picUrl,
groupIds: formData.value?.groupIds,
serialNumber: formData.value?.serialNumber,
locationType: formData.value?.locationType,
longitude: formData.value?.longitude,
latitude: formData.value?.latitude,
};
}
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
@@ -42,8 +91,13 @@ const [Modal, modalApi] = useVbenModal({
return;
}
modalApi.lock();
// 提交表单
const data = (await formApi.getValues()) as IotDeviceApi.Device;
// 合并两个表单的值(字段不冲突,可直接合并)
const basicValues = await formApi.getValues();
const advancedValues = await getAdvancedFormValues();
const data = {
...basicValues,
...advancedValues,
} as IotDeviceApi.DeviceSaveReqVO;
try {
await (formData.value?.id ? updateDevice(data) : createDevice(data));
// 关闭并提示
@@ -57,11 +111,14 @@ const [Modal, modalApi] = useVbenModal({
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
activeKey.value = [];
return;
}
// 加载数据
const data = modalApi.getData<IotDeviceApi.Device>();
const data = modalApi.getData<IotDeviceApi.DeviceRespVO>();
if (!data || !data.id) {
// 新增:确保 Collapse 折叠
activeKey.value = [];
return;
}
// 编辑模式:加载数据
@@ -69,15 +126,43 @@ const [Modal, modalApi] = useVbenModal({
try {
formData.value = await getDevice(data.id);
await formApi.setValues(formData.value);
// 如果存在高级字段数据,自动展开 Collapse
if (
formData.value?.nickname ||
formData.value?.picUrl ||
formData.value?.groupIds?.length ||
formData.value?.serialNumber ||
formData.value?.locationType !== undefined
) {
activeKey.value = ['advanced'];
// 等待 Collapse 展开后表单挂载
await nextTick();
await nextTick();
if (advancedFormApi.isMounted) {
await advancedFormApi.setValues(formData.value);
}
}
} finally {
modalApi.unlock();
}
},
});
/** 初始化产品列表 */
onMounted(async () => {
products.value = await getSimpleProductList();
});
</script>
<template>
<Modal :title="getTitle" class="w-2/5">
<Form class="mx-4" />
<div class="mx-4">
<Form />
<Collapse v-model:active-key="activeKey" class="mt-4">
<Collapse.Panel key="advanced" header="更多设置">
<AdvancedForm />
</Collapse.Panel>
</Collapse>
</div>
</Modal>
</template>

View File

@@ -1,4 +1,6 @@
<script lang="ts" setup>
import type { IotDeviceApi } from '#/api/iot/device/device';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
@@ -41,7 +43,7 @@ const [Modal, modalApi] = useVbenModal({
await updateDeviceGroup({
ids: deviceIds.value,
groupIds: data.groupIds as number[],
});
} as IotDeviceApi.DeviceUpdateGroupReqVO);
// 关闭并提示
await modalApi.close();
emit('success');

View File

@@ -1,5 +1,6 @@
<script lang="ts" setup>
import type { FileType } from 'ant-design-vue/es/upload/interface';
import type { IotDeviceApi } from '#/api/iot/device/device';
import { useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
@@ -38,7 +39,7 @@ const [Modal, modalApi] = useVbenModal({
try {
const result = await importDevice(data.file, data.updateSupport);
// 处理导入结果提示
const importData = result.data || result;
const importData = result as IotDeviceApi.DeviceImportRespVO;
if (importData) {
let text = `上传成功数量:${importData.createDeviceNames?.length || 0};`;
if (importData.createDeviceNames?.length) {

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import type { IotDeviceApi } from '#/api/iot/device/device';
import type { DeviceRespVO, 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<IotDeviceApi.Device[]>([]);
const devices = ref<IotDeviceApi.DeviceRespVO[]>([]);
/** 设备选项 */
const deviceOptions = computed(() => {

View File

@@ -236,6 +236,15 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
name: 'CellImage',
},
},
{
field: 'status',
title: '产品状态',
minWidth: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.IOT_PRODUCT_STATUS },
},
},
{
field: 'createTime',
title: '创建时间',

View File

@@ -115,6 +115,11 @@ onMounted(() => {
<div class="ml-3 min-w-0 flex-1">
<div class="product-title">{{ item.name }}</div>
</div>
<DictTag
:type="DICT_TYPE.IOT_PRODUCT_STATUS"
:value="item.status"
class="status-tag"
/>
</div>
<!-- 内容区域 -->
<div class="mb-3 flex items-start">
@@ -264,6 +269,11 @@ onMounted(() => {
white-space: nowrap;
}
// 状态标签
.status-tag {
font-size: 12px;
}
// 信息列表
.info-list {
.info-item {