feat: 添加设备管理和设备详情页面,解决产品物模型修改乱序问题
- 新增设备详情页面组件和相关功能 - 实现设备信息加载和显示 - 添加运行状态、设备功能、日志管理等子页面 - 优化设备状态展示和操作 - 增加物模型查看功能
This commit is contained in:
parent
60e3f4fc73
commit
1ac9339eb3
|
@ -0,0 +1,62 @@
|
|||
import type { DeviceForm, DeviceQuery, DeviceVO } from './model';
|
||||
|
||||
import type { ID, IDS, PageResult } from '#/api/common';
|
||||
|
||||
import { commonExport } from '#/api/helper';
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
/**
|
||||
* 查询设备列表
|
||||
* @param params
|
||||
* @returns 设备列表
|
||||
*/
|
||||
export function deviceList(params?: DeviceQuery) {
|
||||
return requestClient.get<PageResult<DeviceVO>>('/device/device/list', {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出设备列表
|
||||
* @param params
|
||||
* @returns 设备列表
|
||||
*/
|
||||
export function deviceExport(params?: DeviceQuery) {
|
||||
return commonExport('/device/device/export', params ?? {});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询设备详情
|
||||
* @param id id
|
||||
* @returns 设备详情
|
||||
*/
|
||||
export function deviceInfo(id: ID) {
|
||||
return requestClient.get<DeviceVO>(`/device/device/${id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增设备
|
||||
* @param data
|
||||
* @returns void
|
||||
*/
|
||||
export function deviceAdd(data: DeviceForm) {
|
||||
return requestClient.postWithMsg<void>('/device/device', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新设备
|
||||
* @param data
|
||||
* @returns void
|
||||
*/
|
||||
export function deviceUpdate(data: DeviceForm) {
|
||||
return requestClient.putWithMsg<void>('/device/device', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除设备
|
||||
* @param id id
|
||||
* @returns void
|
||||
*/
|
||||
export function deviceRemove(id: ID | IDS) {
|
||||
return requestClient.deleteWithMsg<void>(`/device/device/${id}`);
|
||||
}
|
|
@ -0,0 +1,147 @@
|
|||
import type { BaseEntity, PageQuery } from '#/api/common';
|
||||
|
||||
export interface DeviceVO {
|
||||
/**
|
||||
* 编号
|
||||
*/
|
||||
id: number | string;
|
||||
|
||||
/**
|
||||
* 设备名称
|
||||
*/
|
||||
deviceName: string;
|
||||
|
||||
/**
|
||||
* 设备类型
|
||||
*/
|
||||
deviceType: string;
|
||||
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
description: string;
|
||||
|
||||
/**
|
||||
* 产品id
|
||||
*/
|
||||
productId: number | string;
|
||||
|
||||
/**
|
||||
* 设备配置
|
||||
*/
|
||||
deviceConf: string;
|
||||
|
||||
/**
|
||||
* 设备物模型
|
||||
*/
|
||||
metadata: string;
|
||||
|
||||
/**
|
||||
* 设备状态
|
||||
*/
|
||||
deviceState: string;
|
||||
|
||||
/**
|
||||
* 注册时间
|
||||
*/
|
||||
registTime: string;
|
||||
|
||||
/**
|
||||
* 父设备id
|
||||
*/
|
||||
parentId: number | string;
|
||||
}
|
||||
|
||||
export interface DeviceForm extends BaseEntity {
|
||||
/**
|
||||
* 编号
|
||||
*/
|
||||
id?: number | string;
|
||||
|
||||
/**
|
||||
* 设备名称
|
||||
*/
|
||||
deviceName?: string;
|
||||
|
||||
/**
|
||||
* 设备类型
|
||||
*/
|
||||
deviceType?: string;
|
||||
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
description?: string;
|
||||
|
||||
/**
|
||||
* 产品id
|
||||
*/
|
||||
productId?: number | string;
|
||||
|
||||
/**
|
||||
* 设备配置
|
||||
*/
|
||||
deviceConf?: string;
|
||||
|
||||
/**
|
||||
* 设备物模型
|
||||
*/
|
||||
metadata?: string;
|
||||
|
||||
/**
|
||||
* 设备状态
|
||||
*/
|
||||
deviceState?: string;
|
||||
|
||||
/**
|
||||
* 注册时间
|
||||
*/
|
||||
registTime?: string;
|
||||
|
||||
/**
|
||||
* 父设备id
|
||||
*/
|
||||
parentId?: number | string;
|
||||
}
|
||||
|
||||
export interface DeviceQuery extends PageQuery {
|
||||
/**
|
||||
* 设备名称
|
||||
*/
|
||||
deviceName?: string;
|
||||
|
||||
/**
|
||||
* 设备类型
|
||||
*/
|
||||
deviceType?: string;
|
||||
|
||||
/**
|
||||
* 产品id
|
||||
*/
|
||||
productId?: number | string;
|
||||
|
||||
/**
|
||||
* 设备物模型
|
||||
*/
|
||||
metadata?: string;
|
||||
|
||||
/**
|
||||
* 设备状态
|
||||
*/
|
||||
deviceState?: string;
|
||||
|
||||
/**
|
||||
* 注册时间
|
||||
*/
|
||||
registTime?: string;
|
||||
|
||||
/**
|
||||
* 父设备id
|
||||
*/
|
||||
parentId?: number | string;
|
||||
|
||||
/**
|
||||
* 日期范围参数
|
||||
*/
|
||||
params?: any;
|
||||
}
|
|
@ -54,13 +54,21 @@ export function productUpdate(data: ProductForm) {
|
|||
|
||||
/**
|
||||
* 更新设备产品(部分)
|
||||
* @param data
|
||||
* @param id data
|
||||
* @returns void
|
||||
*/
|
||||
export function productUpdateById(id: ID, data: ProductForm) {
|
||||
return requestClient.putWithMsg<void>(`/device/product/${id}`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送产品物模型到设备(覆盖)
|
||||
* @param id
|
||||
* @returns void
|
||||
*/
|
||||
export function productPushMetadataById(id: ID) {
|
||||
return requestClient.putWithMsg<void>(`/device/product/pushMetadata/${id}`);
|
||||
}
|
||||
/**
|
||||
* 更新设备产品状态
|
||||
* @param data
|
||||
|
@ -85,8 +93,5 @@ export function productRemove(id: ID | IDS) {
|
|||
* @returns 产品存储策略列表
|
||||
*/
|
||||
export function getPoliciesList(params?: any) {
|
||||
return requestClient.get<PageResult<GatewayVO>>(
|
||||
'/device/product/storage/policies',
|
||||
{ params },
|
||||
);
|
||||
return requestClient.get<PageResult<GatewayVO>>('/device/product/storage/policies', { params });
|
||||
}
|
||||
|
|
|
@ -50,7 +50,7 @@ export const dataTypeOptions = [
|
|||
},
|
||||
{
|
||||
value: 'date',
|
||||
label: 'date(时间型)',
|
||||
label: 'date(时间戳)',
|
||||
},
|
||||
{
|
||||
value: 'enum',
|
||||
|
@ -107,7 +107,13 @@ export const readWriteTypeOptions = [
|
|||
];
|
||||
|
||||
export const timeOptions = [
|
||||
{ label: 'yyyy-MM-dd HH:mm:ss', value: 'yyyy-MM-dd HH:mm:ss' },
|
||||
{ label: 'yyyy-MM-dd', value: 'yyyy-MM-dd' },
|
||||
{ label: 'YYYY-MM-DD HH:mm:ss', value: 'YYYY-MM-DD HH:mm:ss' },
|
||||
{ label: 'YYYY-MM-DD', value: 'YYYY-MM-DD' },
|
||||
{ label: 'HH:mm:ss', value: 'HH:mm:ss' },
|
||||
];
|
||||
|
||||
export const deviceStateOptions = [
|
||||
{ label: '未激活', value: 'notActive', type: 'warning' },
|
||||
{ label: '在线', value: 'online', type: 'success' },
|
||||
{ label: '离线', value: 'offline', type: 'error' },
|
||||
];
|
||||
|
|
|
@ -37,6 +37,11 @@ const routeMetaMapping: Record<string, Omit<RouteMeta, 'title'>> = {
|
|||
requireHomeRedirect: true,
|
||||
},
|
||||
|
||||
'/device/device/detail/:id': {
|
||||
activePath: '/device/device',
|
||||
requireHomeRedirect: true,
|
||||
},
|
||||
|
||||
'/system/oss-config/index': {
|
||||
activePath: '/system/oss',
|
||||
requireHomeRedirect: true,
|
||||
|
@ -64,10 +69,7 @@ const routeMetaMapping: Record<string, Omit<RouteMeta, 'title'>> = {
|
|||
* @param parentPath 上级目录
|
||||
* @returns vben路由
|
||||
*/
|
||||
function backMenuToVbenMenu(
|
||||
menuList: Menu[],
|
||||
parentPath = '',
|
||||
): RouteRecordStringComponent[] {
|
||||
function backMenuToVbenMenu(menuList: Menu[], parentPath = ''): RouteRecordStringComponent[] {
|
||||
const resultList: RouteRecordStringComponent[] = [];
|
||||
menuList.forEach((menu) => {
|
||||
// 根目录为菜单形式
|
||||
|
@ -100,10 +102,7 @@ function backMenuToVbenMenu(
|
|||
|
||||
// 外链: http开头 & 组件为Layout || ParentView
|
||||
// 正则判断是否为http://或者https://开头
|
||||
if (
|
||||
/^https?:\/\//.test(menu.path) &&
|
||||
(menu.component === 'Layout' || menu.component === 'ParentView')
|
||||
) {
|
||||
if (/^https?:\/\//.test(menu.path) && (menu.component === 'Layout' || menu.component === 'ParentView')) {
|
||||
menu.component = 'Link';
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
import { defineStore } from 'pinia';
|
||||
|
||||
import { deviceInfo } from '#/api/device/device';
|
||||
|
||||
export const useDeviceStore = defineStore('device', {
|
||||
state: () => ({
|
||||
currentDevice: {} as any,
|
||||
tabActiveKey: 'BasicInfo',
|
||||
deviceCount: 0,
|
||||
}),
|
||||
|
||||
getters: {
|
||||
getCurrentDevice: (state) => state.currentDevice,
|
||||
getTabActiveKey: (state) => state.tabActiveKey,
|
||||
getDeviceCount: (state) => state.deviceCount,
|
||||
},
|
||||
|
||||
actions: {
|
||||
setCurrent(device: any) {
|
||||
this.currentDevice = device;
|
||||
},
|
||||
|
||||
setTabActiveKey(key: string) {
|
||||
this.tabActiveKey = key;
|
||||
},
|
||||
|
||||
setDeviceCount(count: number) {
|
||||
this.deviceCount = count;
|
||||
},
|
||||
|
||||
async getDetail(id: string) {
|
||||
try {
|
||||
const res = await deviceInfo(id);
|
||||
if (res?.productObj?.productParam) {
|
||||
res.productParamArray = JSON.parse(res.productObj.productParam) || [];
|
||||
}
|
||||
this.currentDevice = res;
|
||||
|
||||
return res;
|
||||
} catch (error) {
|
||||
console.error('获取设备详情失败:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.currentDevice = {};
|
||||
this.tabActiveKey = 'BasicInfo';
|
||||
this.deviceCount = 0;
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,126 @@
|
|||
import type { FormSchemaGetter } from '#/adapter/form';
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { deviceStateOptions, deviceTypeOptions } from '#/constants/dicts';
|
||||
|
||||
export const querySchema: FormSchemaGetter = () => [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'deviceKey',
|
||||
label: '设备KEY',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'deviceName',
|
||||
label: '设备名称',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'productId',
|
||||
label: '所属产品',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: deviceStateOptions,
|
||||
},
|
||||
fieldName: 'deviceState',
|
||||
label: '设备状态',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: deviceTypeOptions,
|
||||
},
|
||||
fieldName: 'deviceType',
|
||||
label: '设备类型',
|
||||
},
|
||||
];
|
||||
|
||||
// 需要使用i18n注意这里要改成getter形式 否则切换语言不会刷新
|
||||
// export const columns: () => VxeGridProps['columns'] = () => [
|
||||
export const columns: VxeGridProps['columns'] = [
|
||||
{ type: 'checkbox', width: 60 },
|
||||
// {
|
||||
// title: '编号',
|
||||
// field: 'id',
|
||||
// },
|
||||
{
|
||||
title: '设备KEY',
|
||||
field: 'deviceKey',
|
||||
},
|
||||
{
|
||||
title: '设备名称',
|
||||
field: 'deviceName',
|
||||
},
|
||||
{
|
||||
title: '所属产品',
|
||||
field: 'productName',
|
||||
},
|
||||
{
|
||||
title: '设备类型',
|
||||
field: 'deviceType',
|
||||
slots: { default: 'deviceType' },
|
||||
},
|
||||
{
|
||||
title: '设备状态',
|
||||
field: 'deviceState',
|
||||
slots: { default: 'deviceState' },
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
field: 'description',
|
||||
},
|
||||
{
|
||||
field: 'action',
|
||||
fixed: 'right',
|
||||
slots: { default: 'action' },
|
||||
title: '操作',
|
||||
width: 180,
|
||||
},
|
||||
];
|
||||
|
||||
export const drawerSchema: FormSchemaGetter = () => [
|
||||
{
|
||||
label: '编号',
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
show: () => false,
|
||||
triggerFields: [''],
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '设备KEY',
|
||||
fieldName: 'deviceKey',
|
||||
component: 'Input',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
label: '设备名称',
|
||||
fieldName: 'deviceName',
|
||||
component: 'Input',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
label: '所属产品',
|
||||
fieldName: 'productId',
|
||||
component: 'Select',
|
||||
componentProps: {},
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
{
|
||||
label: '设备图片',
|
||||
fieldName: 'imgId',
|
||||
component: 'ImageUpload',
|
||||
componentProps: {
|
||||
// accept: 'image/*', // 可选拓展名或者mime类型 ,拼接
|
||||
// maxCount: 1, // 最大上传文件数 默认为1 为1会绑定为string而非string[]类型
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '描述',
|
||||
fieldName: 'description',
|
||||
component: 'Textarea',
|
||||
},
|
||||
];
|
|
@ -0,0 +1,222 @@
|
|||
<script setup lang="ts">
|
||||
import type { DeviceVO } from '#/api/device/device/model';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenDrawer } from '@vben/common-ui';
|
||||
|
||||
import { EditOutlined, EyeOutlined } from '@ant-design/icons-vue';
|
||||
import { Button, Descriptions, DescriptionsItem, Drawer, Space } from 'ant-design-vue';
|
||||
|
||||
import { deviceTypeOptions, networkTypeOptions } from '#/constants/dicts';
|
||||
import TSLViewer from '#/views/device/product/detail/components/TSLViewer.vue';
|
||||
|
||||
import deviceDrawer from '../../device-drawer.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
deviceInfo: DeviceVO;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
refresh: [];
|
||||
}>();
|
||||
|
||||
const [DeviceDrawer, drawerApi] = useVbenDrawer({
|
||||
connectedComponent: deviceDrawer,
|
||||
});
|
||||
|
||||
const tslVisible = ref(false);
|
||||
|
||||
// 编辑设备信息
|
||||
const handleEdit = () => {
|
||||
drawerApi.setData({ id: props.deviceInfo.id });
|
||||
drawerApi.open();
|
||||
};
|
||||
|
||||
// 查看物模型TSL
|
||||
const handleViewTSL = () => {
|
||||
tslVisible.value = true;
|
||||
};
|
||||
|
||||
// 关闭TSL抽屉
|
||||
const handleTSLClose = () => {
|
||||
tslVisible.value = false;
|
||||
};
|
||||
|
||||
const metadataObj = computed(() => {
|
||||
return JSON.parse(props.deviceInfo.metadata || '{}');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="basic-info">
|
||||
<div class="info-header">
|
||||
<h3>设备信息</h3>
|
||||
<Space>
|
||||
<Button type="link" size="small" @click="handleEdit" v-access:code="['device:device:edit']">
|
||||
<template #icon>
|
||||
<EditOutlined />
|
||||
</template>
|
||||
编辑
|
||||
</Button>
|
||||
<Button type="link" size="small" @click="handleViewTSL">
|
||||
<template #icon>
|
||||
<EyeOutlined />
|
||||
</template>
|
||||
物模型
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
<div class="info-content">
|
||||
<Descriptions :column="3" bordered>
|
||||
<!-- <DescriptionsItem label="设备ID">
|
||||
{{ deviceInfo.id }}
|
||||
</DescriptionsItem> -->
|
||||
<DescriptionsItem label="设备KEY">
|
||||
{{ deviceInfo.deviceKey }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="设备名称">
|
||||
{{ deviceInfo.deviceName }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="设备类型">
|
||||
{{ deviceTypeOptions.find((option) => option.value === deviceInfo.deviceType)?.label }}
|
||||
</DescriptionsItem>
|
||||
<!-- <DescriptionsItem label="产品名称">
|
||||
{{ deviceInfo.productName }}
|
||||
</DescriptionsItem> -->
|
||||
<DescriptionsItem label="固件版本">
|
||||
{{ deviceInfo.otaVersion || '-' }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="接入方式">
|
||||
{{ networkTypeOptions.find((option) => option.value === deviceInfo?.productObj?.provider)?.label || '-' }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="消息协议">
|
||||
{{ deviceInfo?.productObj?.protocolName || '-' }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="创建时间">
|
||||
{{ deviceInfo.createTime }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="注册时间">
|
||||
{{ deviceInfo.registTime }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="最后上线时间">
|
||||
{{ deviceInfo.updateTime }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="父设备" v-if="deviceInfo.parentId">
|
||||
{{ deviceInfo.parentName }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="说明" :span="3">
|
||||
{{ deviceInfo.description }}
|
||||
</DescriptionsItem>
|
||||
</Descriptions>
|
||||
</div>
|
||||
|
||||
<!-- 产品参数展示 -->
|
||||
<div class="product-params-section">
|
||||
<div class="section-header">
|
||||
<h3>设备参数</h3>
|
||||
</div>
|
||||
<Descriptions bordered :column="3">
|
||||
<DescriptionsItem v-for="param in deviceInfo.productParamArray" :key="param.key" :label="param.label">
|
||||
{{ param.value }}
|
||||
</DescriptionsItem>
|
||||
</Descriptions>
|
||||
</div>
|
||||
|
||||
<DeviceDrawer @reload="emit('refresh')" />
|
||||
|
||||
<!-- TSL查看抽屉 -->
|
||||
<Drawer v-model:open="tslVisible" title="物模型" width="800px" @close="handleTSLClose">
|
||||
<TSLViewer
|
||||
:product-id="deviceInfo?.productId"
|
||||
:product-info="deviceInfo?.productObj"
|
||||
:file-name="deviceInfo?.deviceName"
|
||||
:metadata="metadataObj"
|
||||
/>
|
||||
</Drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.basic-info {
|
||||
.info-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
}
|
||||
|
||||
.info-content {
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.product-params-section {
|
||||
padding: 16px 0;
|
||||
margin-top: 24px;
|
||||
border-radius: 6px;
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-params {
|
||||
padding: 20px 0;
|
||||
color: #8c8c8c;
|
||||
text-align: center;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.params-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 12px;
|
||||
|
||||
.param-card {
|
||||
padding: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 4px;
|
||||
|
||||
.param-key {
|
||||
margin-bottom: 4px;
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.param-label {
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.param-value {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,571 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
|
||||
import { EllipsisText } from '@vben/common-ui';
|
||||
|
||||
import { LineChartOutlined, UnorderedListOutlined } from '@ant-design/icons-vue';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Checkbox,
|
||||
Col,
|
||||
DatePicker,
|
||||
Empty,
|
||||
Modal,
|
||||
RadioButton,
|
||||
RadioGroup,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
Tabs,
|
||||
} from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
interface Props {
|
||||
deviceId: string;
|
||||
deviceInfo: any;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const loading = ref(false);
|
||||
const selectedGroup = ref('all');
|
||||
const selectedTypes = ref(['R', 'RW']); // 默认选中读写和只读
|
||||
const metadata = ref({ properties: [], propertyGroups: [] });
|
||||
|
||||
// // 物模型数据
|
||||
// const metadata = computed(() => {
|
||||
// if (props.deviceInfo?.productObj?.metadata) {
|
||||
// try {
|
||||
// return JSON.parse(props.deviceInfo.productObj.metadata);
|
||||
// } catch {
|
||||
// return { properties: [], propertyGroups: [] };
|
||||
// }
|
||||
// }
|
||||
// return { properties: [], propertyGroups: [] };
|
||||
// });
|
||||
|
||||
const getMetadata = () => {
|
||||
let metadataObj = { properties: [], propertyGroups: [] };
|
||||
if (props.deviceInfo?.productObj?.metadata) {
|
||||
metadataObj = JSON.parse(props.deviceInfo.productObj.metadata);
|
||||
}
|
||||
metadataObj.properties = metadataObj.properties.map((prop: any) => ({
|
||||
...prop,
|
||||
value: null,
|
||||
timestamp: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||
}));
|
||||
metadata.value = metadataObj;
|
||||
};
|
||||
|
||||
// 属性分组选项
|
||||
const groupOptions = computed(() => {
|
||||
const groups = metadata.value.propertyGroups || [];
|
||||
return [
|
||||
{ label: '全部', value: 'all' },
|
||||
...groups.map((group: any) => ({
|
||||
label: group.name,
|
||||
value: group.id,
|
||||
})),
|
||||
];
|
||||
});
|
||||
|
||||
// 过滤后的属性列表
|
||||
const filteredProperties = computed(() => {
|
||||
let properties = metadata.value.properties || [];
|
||||
|
||||
// 按分组筛选
|
||||
if (selectedGroup.value !== 'all') {
|
||||
const group = metadata.value.propertyGroups?.find((g: any) => g.id === selectedGroup.value);
|
||||
if (group?.properties) {
|
||||
const groupPropertyIds = new Set(group.properties.map((p: any) => p.id));
|
||||
properties = properties.filter((p: any) => groupPropertyIds.has(p.id));
|
||||
}
|
||||
}
|
||||
|
||||
// 按读写类型筛选
|
||||
properties = properties.filter((p: any) => {
|
||||
const type = p.expands?.type || 'R';
|
||||
return selectedTypes.value.includes(type);
|
||||
});
|
||||
|
||||
// 为每个属性添加value字段
|
||||
return properties;
|
||||
});
|
||||
|
||||
// 属性日志弹窗
|
||||
const logModalVisible = ref(false);
|
||||
const currentProperty = ref<any>(null);
|
||||
const logTabActiveKey = ref('list');
|
||||
const activeQuickTime = ref('today');
|
||||
const logDateRange = ref([dayjs().startOf('day'), dayjs()]);
|
||||
const logLoading = ref(false);
|
||||
const logData = ref<any[]>([]);
|
||||
|
||||
// 日志表格列配置
|
||||
const logColumns = [
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'timestamp',
|
||||
key: 'timestamp',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: '值',
|
||||
dataIndex: 'value',
|
||||
key: 'value',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 100,
|
||||
},
|
||||
];
|
||||
|
||||
// 打开属性日志弹窗
|
||||
const openPropertyLog = (property: any) => {
|
||||
currentProperty.value = property;
|
||||
logModalVisible.value = true;
|
||||
loadPropertyLog();
|
||||
};
|
||||
|
||||
// 加载属性日志数据
|
||||
const loadPropertyLog = async () => {
|
||||
if (!currentProperty.value) return;
|
||||
|
||||
try {
|
||||
logLoading.value = true;
|
||||
// TODO: 调用API获取属性日志数据
|
||||
// const res = await getPropertyLog({
|
||||
// deviceId: props.deviceId,
|
||||
// propertyId: currentProperty.value.id,
|
||||
// startTime: logDateRange.value[0]?.format('YYYY-MM-DD HH:mm:ss'),
|
||||
// endTime: logDateRange.value[1]?.format('YYYY-MM-DD HH:mm:ss')
|
||||
// });
|
||||
// logData.value = res.data || [];
|
||||
|
||||
// 模拟数据
|
||||
logData.value = [
|
||||
{ timestamp: '2025-08-19 16:19:40', value: '0.5' },
|
||||
{ timestamp: '2025-08-19 16:18:40', value: '0.4' },
|
||||
{ timestamp: '2025-08-19 16:17:40', value: '0.3' },
|
||||
];
|
||||
} catch (error) {
|
||||
console.error('获取属性日志失败:', error);
|
||||
} finally {
|
||||
logLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 时间范围变化
|
||||
const handleDateRangeChange = (dates: any) => {
|
||||
logDateRange.value = dates;
|
||||
if (dates && dates.length === 2) {
|
||||
loadPropertyLog();
|
||||
}
|
||||
};
|
||||
|
||||
// 快速时间选择
|
||||
const handleQuickTimeSelect = (type: string) => {
|
||||
console.log(type);
|
||||
const now = dayjs();
|
||||
switch (type.target.value) {
|
||||
case 'month': {
|
||||
logDateRange.value = [now.subtract(1, 'month'), now];
|
||||
break;
|
||||
}
|
||||
case 'today': {
|
||||
logDateRange.value = [now.startOf('day'), now];
|
||||
break;
|
||||
}
|
||||
case 'week': {
|
||||
logDateRange.value = [now.subtract(7, 'day'), now];
|
||||
break;
|
||||
}
|
||||
}
|
||||
loadPropertyLog();
|
||||
};
|
||||
|
||||
// 格式化数值显示
|
||||
const formatValue = (property: any) => {
|
||||
const { value, valueParams } = property;
|
||||
if (value === undefined || value === null) return '--';
|
||||
|
||||
let displayValue = value;
|
||||
|
||||
if (valueParams?.formType === 'input' && valueParams?.length) {
|
||||
displayValue = value.slice(0, valueParams.length);
|
||||
}
|
||||
|
||||
// 根据数据类型格式化
|
||||
if (valueParams?.formType === 'switch') {
|
||||
return value.toString() === valueParams.trueValue ? valueParams.trueText : valueParams.falseText;
|
||||
}
|
||||
|
||||
if (valueParams?.formType === 'select') {
|
||||
return valueParams.enumConf.find((item: any) => item.value === value)?.text;
|
||||
}
|
||||
|
||||
if (valueParams?.formType === 'time') {
|
||||
return dayjs(value).format(valueParams.format);
|
||||
}
|
||||
|
||||
if (valueParams?.formType === 'number' || valueParams?.formType === 'progress') {
|
||||
if (valueParams?.scale) {
|
||||
return value.toFixed(valueParams.scale);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// displayValue = `${value}`;
|
||||
|
||||
return displayValue;
|
||||
};
|
||||
|
||||
// 判断是否显示图表
|
||||
const canShowChart = (property: any) => {
|
||||
const { valueParams } = property;
|
||||
const numericTypes = ['int', 'float', 'double'];
|
||||
return numericTypes.includes(valueParams?.dataType);
|
||||
};
|
||||
|
||||
// 关闭日志弹窗
|
||||
const closeLogModal = () => {
|
||||
logModalVisible.value = false;
|
||||
currentProperty.value = null;
|
||||
logData.value = [];
|
||||
};
|
||||
|
||||
// 定时刷新数据
|
||||
let refreshTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
const generateRandomString = (length: number) => {
|
||||
// 定义字符池
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
// 从字符池中随机选取一个字符
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const startRefreshTimer = () => {
|
||||
refreshTimer = setInterval(() => {
|
||||
// TODO: 刷新属性值
|
||||
console.log('刷新属性值');
|
||||
metadata.value.properties.forEach((item: any) => {
|
||||
switch (item.valueParams?.dataType) {
|
||||
case 'boolean': {
|
||||
item.value = Math.random() > 0.5 ? 'true' : 'false';
|
||||
|
||||
break;
|
||||
}
|
||||
case 'date': {
|
||||
item.value = Date.now();
|
||||
|
||||
break;
|
||||
}
|
||||
case 'double':
|
||||
case 'float':
|
||||
case 'int':
|
||||
case 'long': {
|
||||
item.value = Math.random() * 100;
|
||||
|
||||
break;
|
||||
}
|
||||
case 'enum': {
|
||||
item.value = item.valueParams.enumConf[Math.floor(Math.random() * item.valueParams.enumConf.length)].value;
|
||||
|
||||
break;
|
||||
}
|
||||
case 'string': {
|
||||
item.value = generateRandomString(Math.floor(Math.random() * 1_000_000));
|
||||
|
||||
break;
|
||||
}
|
||||
// No default
|
||||
}
|
||||
item.timestamp = dayjs().format('YYYY-MM-DD HH:mm:ss');
|
||||
});
|
||||
console.log(metadata.value.properties);
|
||||
}, 3000); // 30秒刷新一次
|
||||
};
|
||||
|
||||
const stopRefreshTimer = () => {
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer);
|
||||
refreshTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
startRefreshTimer();
|
||||
getMetadata();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
stopRefreshTimer();
|
||||
});
|
||||
|
||||
// 监听设备信息变化
|
||||
watch(
|
||||
() => props.deviceInfo,
|
||||
() => {
|
||||
// 设备信息变化时重新加载数据
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="running-status">
|
||||
<!-- 控制面板 -->
|
||||
<div class="control-panel">
|
||||
<div class="control-item">
|
||||
<span class="control-label">属性分组:</span>
|
||||
<Select
|
||||
v-model:value="selectedGroup"
|
||||
:options="groupOptions"
|
||||
style="width: 200px"
|
||||
placeholder="请选择属性分组"
|
||||
/>
|
||||
</div>
|
||||
<div class="control-item">
|
||||
<span class="control-label">访问权限:</span>
|
||||
<Checkbox.Group v-model:value="selectedTypes">
|
||||
<Checkbox value="RW">读写</Checkbox>
|
||||
<Checkbox value="R">只读</Checkbox>
|
||||
<Checkbox value="W">只写</Checkbox>
|
||||
</Checkbox.Group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 属性卡片网格 -->
|
||||
<div class="property-grid">
|
||||
<Row :gutter="16">
|
||||
<Col v-for="property in filteredProperties" :key="property.id" :span="6" class="property-col">
|
||||
<Card class="property-card" :loading="loading">
|
||||
<div class="property-header">
|
||||
<h4 class="property-title">{{ property.name }}</h4>
|
||||
<Button type="text" size="small" class="property-log-btn" @click="openPropertyLog(property)">
|
||||
<UnorderedListOutlined />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="property-content">
|
||||
<EllipsisText class="property-value" :line="3" expand>
|
||||
{{ formatValue(property) }}
|
||||
</EllipsisText>
|
||||
<div class="property-unit">
|
||||
{{ property.valueParams?.unit || '' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="property-footer">
|
||||
<span class="property-timestamp">{{ property.timestamp }}</span>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="filteredProperties.length === 0" class="empty-state">
|
||||
<Empty description="暂无属性数据" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 属性日志弹窗 -->
|
||||
<Modal
|
||||
v-model:open="logModalVisible"
|
||||
:title="`${currentProperty?.name || ''} - 详情`"
|
||||
width="800px"
|
||||
@cancel="closeLogModal"
|
||||
:footer="null"
|
||||
>
|
||||
<div class="log-modal-content">
|
||||
<!-- 时间选择 -->
|
||||
<div class="time-selection">
|
||||
<Space>
|
||||
<RadioGroup v-model:value="activeQuickTime" @change="handleQuickTimeSelect" button-style="solid">
|
||||
<RadioButton value="today">今日</RadioButton>
|
||||
<RadioButton value="week">近一周</RadioButton>
|
||||
<RadioButton value="month">近一月</RadioButton>
|
||||
</RadioGroup>
|
||||
<!-- <Button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleQuickTimeSelect('today')"
|
||||
>
|
||||
今日
|
||||
</Button>
|
||||
<Button size="small" @click="handleQuickTimeSelect('week')">
|
||||
近一周
|
||||
</Button>
|
||||
<Button size="small" @click="handleQuickTimeSelect('month')">
|
||||
近一月
|
||||
</Button> -->
|
||||
<DatePicker.RangePicker
|
||||
v-model:value="logDateRange"
|
||||
:show-time="true"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
@change="handleDateRangeChange"
|
||||
/>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<!-- 标签页 -->
|
||||
<Tabs v-model:active-key="logTabActiveKey" class="log-tabs">
|
||||
<Tabs.TabPane key="list" tab="列表">
|
||||
<Table
|
||||
:columns="logColumns"
|
||||
:data-source="logData"
|
||||
:loading="logLoading"
|
||||
:pagination="false"
|
||||
size="small"
|
||||
row-key="timestamp"
|
||||
>
|
||||
<template #bodyCell="{ column }">
|
||||
<template v-if="column.key === 'action'">
|
||||
<Button type="link" size="small">查看</Button>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane v-if="currentProperty && canShowChart(currentProperty)" key="chart" tab="图表">
|
||||
<div class="chart-container">
|
||||
<div class="chart-placeholder">
|
||||
<LineChartOutlined style="font-size: 48px; color: #d9d9d9" />
|
||||
<p>图表功能开发中...</p>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.running-status {
|
||||
.control-panel {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.control-item {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
|
||||
.control-label {
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.property-grid {
|
||||
.property-col {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.property-card {
|
||||
height: 100%;
|
||||
|
||||
.property-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.property-title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.property-log-btn {
|
||||
padding: 4px;
|
||||
color: #8c8c8c;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.property-content {
|
||||
position: relative;
|
||||
|
||||
.property-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 80px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.property-unit {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
font-size: 20px;
|
||||
color: #8c8c8c;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.property-info {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.property-footer {
|
||||
.property-timestamp {
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 60px 0;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.log-modal-content {
|
||||
.time-selection {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.log-tabs {
|
||||
.chart-container {
|
||||
.chart-placeholder {
|
||||
padding: 60px 0;
|
||||
color: #8c8c8c;
|
||||
text-align: center;
|
||||
|
||||
p {
|
||||
margin: 16px 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-card-body) {
|
||||
padding: 12px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,274 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { ArrowLeftOutlined } from '@ant-design/icons-vue';
|
||||
import { message, Switch, TabPane, Tabs } from 'ant-design-vue';
|
||||
|
||||
import { deviceStateOptions } from '#/constants/dicts';
|
||||
// import { deviceUpdateStatus } from '#/api/device/device';
|
||||
import { useDeviceStore } from '#/store/device';
|
||||
|
||||
import BasicInfo from './components/BasicInfo.vue';
|
||||
import DeviceFunction from './components/DeviceFunction.vue';
|
||||
import LogManagement from './components/LogManagement.vue';
|
||||
import RunningStatus from './components/RunningStatus.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const deviceStore = useDeviceStore();
|
||||
|
||||
const deviceId = computed(() => route.params.id as string);
|
||||
const activeTab = computed(() => deviceStore.getTabActiveKey);
|
||||
const currentDevice = computed(() => deviceStore.getCurrentDevice);
|
||||
|
||||
// 加载设备信息
|
||||
const loadDeviceInfo = async () => {
|
||||
try {
|
||||
await deviceStore.getDetail(deviceId.value);
|
||||
} catch {
|
||||
message.error('加载设备信息失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 处理状态变更
|
||||
const handleStatusChange = async (checked: boolean) => {
|
||||
try {
|
||||
console.log('checked', checked);
|
||||
// await deviceUpdateStatus({
|
||||
// id: deviceId.value,
|
||||
// enabled: checked,
|
||||
// });
|
||||
// await loadDeviceInfo();
|
||||
} catch {
|
||||
message.error('状态更新失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 返回上一页
|
||||
const goBack = () => {
|
||||
router.back();
|
||||
};
|
||||
|
||||
// 标签页切换
|
||||
const handleTabChange = (key: string) => {
|
||||
deviceStore.setTabActiveKey(key);
|
||||
if (key === 'BasicInfo') {
|
||||
loadDeviceInfo();
|
||||
}
|
||||
};
|
||||
|
||||
const handleProductClick = () => {
|
||||
router.push(`/device/product/detail/${currentDevice.value.productId}`);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadDeviceInfo();
|
||||
});
|
||||
|
||||
// 组件销毁时重置 store
|
||||
onUnmounted(() => {
|
||||
deviceStore.reset();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page :auto-content-height="true">
|
||||
<div class="device-detail">
|
||||
<!-- 页面头部 -->
|
||||
<div class="detail-header">
|
||||
<div class="header-left">
|
||||
<a-button @click="goBack" class="back-btn">
|
||||
<template #icon>
|
||||
<ArrowLeftOutlined />
|
||||
</template>
|
||||
返回
|
||||
</a-button>
|
||||
<div class="device-info">
|
||||
<h2 class="device-name">
|
||||
{{ currentDevice.deviceName || deviceId }}
|
||||
</h2>
|
||||
<div class="device-status">
|
||||
<span class="status-label">在线状态:</span>
|
||||
<!-- prettier-ignore -->
|
||||
<span
|
||||
class="status-dot"
|
||||
:class="deviceStateOptions.find((item) => item.value === currentDevice.deviceState)?.type"
|
||||
>•</span>
|
||||
<span class="status-text">
|
||||
{{ deviceStateOptions.find((item) => item.value === currentDevice.deviceState)?.label }}
|
||||
</span>
|
||||
<Switch
|
||||
:checked="currentDevice.enabled === '1'"
|
||||
checked-children="启用"
|
||||
un-checked-children="禁用"
|
||||
checked-value="1"
|
||||
un-checked-value="0"
|
||||
@change="handleStatusChange"
|
||||
class="status-switch"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<!-- <a-button
|
||||
type="primary"
|
||||
@click="handleStatusChange(!(currentDevice.enabled === '1'))"
|
||||
v-access:code="['device:device:edit']"
|
||||
>
|
||||
{{ currentDevice.enabled === '1' ? '禁用设备' : '启用设备' }}
|
||||
</a-button> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 设备基本信息 -->
|
||||
<div class="device-basic">
|
||||
<div class="basic-item">
|
||||
<span class="basic-label">ID:</span>
|
||||
<span class="basic-value">{{ deviceId }}</span>
|
||||
</div>
|
||||
<div class="basic-item">
|
||||
<span class="basic-label">所属产品:</span>
|
||||
<a class="basic-value product-link" @click="handleProductClick">{{
|
||||
currentDevice?.productObj?.productName || '未知产品'
|
||||
}}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 标签页内容 -->
|
||||
<Tabs v-model:active-key="activeTab" class="detail-tabs" @change="handleTabChange">
|
||||
<TabPane key="BasicInfo" tab="实例信息">
|
||||
<BasicInfo :device-info="currentDevice" @refresh="loadDeviceInfo" />
|
||||
</TabPane>
|
||||
<TabPane key="RunningStatus" tab="运行状态">
|
||||
<RunningStatus :device-id="deviceId" :device-info="currentDevice" />
|
||||
</TabPane>
|
||||
<TabPane key="DeviceFunction" tab="设备功能">
|
||||
<DeviceFunction :device-id="deviceId" :device-info="currentDevice" />
|
||||
</TabPane>
|
||||
<TabPane key="LogManagement" tab="日志管理">
|
||||
<LogManagement :device-id="deviceId" :device-info="currentDevice" />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.device-detail {
|
||||
padding: 24px;
|
||||
background: #fff;
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 16px;
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
|
||||
.back-btn {
|
||||
// margin-right: 16px;
|
||||
}
|
||||
|
||||
.device-info {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
|
||||
.device-name {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.device-status {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
.status-label {
|
||||
font-size: 14px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 20px;
|
||||
margin-top: -5px;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
line-height: 20px;
|
||||
|
||||
&.warning {
|
||||
color: #faad14;
|
||||
}
|
||||
|
||||
&.success {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
&.error {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 14px;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.status-switch {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.device-basic {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
padding: 16px 0;
|
||||
|
||||
.basic-item {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
.basic-label {
|
||||
font-size: 14px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.basic-value {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
|
||||
&.product-link {
|
||||
color: #1890ff;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.detail-tabs {
|
||||
:deep(.ant-tabs-content-holder) {
|
||||
padding-top: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,141 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenDrawer } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
import { cloneDeep } from '@vben/utils';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { deviceAdd, deviceInfo, deviceUpdate } from '#/api/device/device';
|
||||
import { productList } from '#/api/device/product';
|
||||
import { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';
|
||||
|
||||
import { drawerSchema } from './data';
|
||||
|
||||
const emit = defineEmits<{ reload: [] }>();
|
||||
|
||||
const isUpdate = ref(false);
|
||||
const title = computed(() => {
|
||||
return isUpdate.value ? $t('pages.common.edit') : $t('pages.common.add');
|
||||
});
|
||||
|
||||
const [BasicForm, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
// 默认占满两列
|
||||
formItemClass: 'col-span-2',
|
||||
// 默认label宽度 px
|
||||
labelWidth: 80,
|
||||
// 通用配置项 会影响到所有表单项
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
schema: drawerSchema(),
|
||||
showDefaultActions: false,
|
||||
wrapperClass: 'grid-cols-2',
|
||||
});
|
||||
|
||||
const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff({
|
||||
initializedGetter: defaultFormValueGetter(formApi),
|
||||
currentGetter: defaultFormValueGetter(formApi),
|
||||
});
|
||||
|
||||
const [BasicDrawer, drawerApi] = useVbenDrawer({
|
||||
// 在这里更改宽度
|
||||
class: 'w-[550px]',
|
||||
fullscreenButton: false,
|
||||
onBeforeClose,
|
||||
onClosed: handleClosed,
|
||||
onConfirm: handleConfirm,
|
||||
onOpenChange: async (isOpen) => {
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
drawerApi.drawerLoading(true);
|
||||
|
||||
const { id } = drawerApi.getData() as { id?: number | string };
|
||||
isUpdate.value = !!id;
|
||||
formApi.updateSchema([
|
||||
{ componentProps: { disabled: isUpdate.value }, fieldName: 'deviceKey' },
|
||||
{ componentProps: { disabled: isUpdate.value }, fieldName: 'productId' },
|
||||
]);
|
||||
|
||||
if (isUpdate.value && id) {
|
||||
const record = await deviceInfo(id);
|
||||
await formApi.setValues(record);
|
||||
}
|
||||
await markInitialized();
|
||||
|
||||
drawerApi.drawerLoading(false);
|
||||
getProductOptionList();
|
||||
},
|
||||
});
|
||||
|
||||
async function handleConfirm() {
|
||||
try {
|
||||
drawerApi.lock(true);
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
// getValues获取为一个readonly的对象 需要修改必须先深拷贝一次
|
||||
const data = cloneDeep(await formApi.getValues());
|
||||
await (isUpdate.value ? deviceUpdate(data) : deviceAdd(data));
|
||||
resetInitialized();
|
||||
emit('reload');
|
||||
drawerApi.close();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
drawerApi.lock(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleClosed() {
|
||||
await formApi.resetForm();
|
||||
resetInitialized();
|
||||
}
|
||||
|
||||
async function getProductOptionList() {
|
||||
try {
|
||||
const res = await productList({
|
||||
pageNum: 1,
|
||||
pageSize: 1000,
|
||||
enabled: '1',
|
||||
});
|
||||
|
||||
const productOptions = res.rows.map((item) => ({
|
||||
label: item.productName,
|
||||
value: item.id,
|
||||
imgId: item.imgId,
|
||||
}));
|
||||
|
||||
const placeholder = productOptions.length > 0 ? '请选择' : '暂无产品';
|
||||
|
||||
formApi.updateSchema([
|
||||
{
|
||||
componentProps: {
|
||||
options: productOptions || [],
|
||||
placeholder,
|
||||
onChange: (value: number | string) => {
|
||||
// 当产品选择变化时,自动设置设备图片
|
||||
if (value) {
|
||||
const selectedProduct = productOptions.find((option) => option.value === value);
|
||||
formApi.setFieldValue('imgId', selectedProduct?.imgId || '');
|
||||
}
|
||||
},
|
||||
},
|
||||
fieldName: 'productId',
|
||||
},
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('获取产品列表失败:', error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicDrawer :title="title">
|
||||
<BasicForm />
|
||||
</BasicDrawer>
|
||||
</template>
|
|
@ -0,0 +1,212 @@
|
|||
<script setup lang="ts">
|
||||
import type { VbenFormProps } from '@vben/common-ui';
|
||||
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
import type { DeviceForm } from '#/api/device/device/model';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { Page, useVbenDrawer } from '@vben/common-ui';
|
||||
import { getVxePopupContainer } from '@vben/utils';
|
||||
|
||||
import { Modal, Popconfirm, Space, Tag } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';
|
||||
import { deviceExport, deviceList, deviceRemove } from '#/api/device/device';
|
||||
import { productList } from '#/api/device/product';
|
||||
import { deviceStateOptions, deviceTypeOptions } from '#/constants/dicts';
|
||||
import { commonDownloadExcel } from '#/utils/file/download';
|
||||
|
||||
import { columns, querySchema } from './data';
|
||||
import deviceDrawer from './device-drawer.vue';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const productOptions = ref<{ label: string; value: string }[]>([]);
|
||||
|
||||
const formOptions: VbenFormProps = {
|
||||
commonConfig: {
|
||||
labelWidth: 80,
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
schema: querySchema(),
|
||||
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
|
||||
// 处理区间选择器RangePicker时间格式 将一个字段映射为两个字段 搜索/导出会用到
|
||||
// 不需要直接删除
|
||||
// fieldMappingTime: [
|
||||
// [
|
||||
// 'createTime',
|
||||
// ['params[beginTime]', 'params[endTime]'],
|
||||
// ['YYYY-MM-DD 00:00:00', 'YYYY-MM-DD 23:59:59'],
|
||||
// ],
|
||||
// ],
|
||||
};
|
||||
|
||||
const gridOptions: VxeGridProps = {
|
||||
checkboxConfig: {
|
||||
// 高亮
|
||||
highlight: true,
|
||||
// 翻页时保留选中状态
|
||||
reserve: true,
|
||||
// 点击行选中
|
||||
// trigger: 'row',
|
||||
},
|
||||
// 需要使用i18n注意这里要改成getter形式 否则切换语言不会刷新
|
||||
// columns: columns(),
|
||||
columns,
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues = {}) => {
|
||||
return await deviceList({
|
||||
pageNum: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
// 表格全局唯一表示 保存列配置需要用到
|
||||
id: 'device-device-index',
|
||||
};
|
||||
|
||||
const [BasicTable, tableApi] = useVbenVxeGrid({
|
||||
formOptions,
|
||||
gridOptions,
|
||||
});
|
||||
|
||||
const [DeviceDrawer, drawerApi] = useVbenDrawer({
|
||||
connectedComponent: deviceDrawer,
|
||||
});
|
||||
|
||||
function handleAdd() {
|
||||
drawerApi.setData({});
|
||||
drawerApi.open();
|
||||
}
|
||||
|
||||
// 查看设备详情
|
||||
async function handleView(row: any) {
|
||||
router.push(`/device/device/detail/${row.id}`);
|
||||
}
|
||||
|
||||
// 编辑设备
|
||||
async function handleEdit(row: any) {
|
||||
drawerApi.setData({ id: row.id });
|
||||
drawerApi.open();
|
||||
}
|
||||
|
||||
async function handleDelete(row: Required<DeviceForm>) {
|
||||
await deviceRemove(row.id);
|
||||
await tableApi.query();
|
||||
}
|
||||
|
||||
function handleMultiDelete() {
|
||||
const rows = tableApi.grid.getCheckboxRecords();
|
||||
const ids = rows.map((row: Required<DeviceForm>) => row.id);
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
okType: 'danger',
|
||||
content: `确认删除选中的${ids.length}条记录吗?`,
|
||||
onOk: async () => {
|
||||
await deviceRemove(ids);
|
||||
await tableApi.query();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function handleDownloadExcel() {
|
||||
commonDownloadExcel(deviceExport, '设备数据', tableApi.formApi.form.values, {
|
||||
fieldMappingTime: formOptions.fieldMappingTime,
|
||||
});
|
||||
}
|
||||
|
||||
async function getProductOptionList() {
|
||||
try {
|
||||
const res = await productList({
|
||||
pageNum: 1,
|
||||
pageSize: 1000,
|
||||
});
|
||||
productOptions.value = res.rows.map((item) => ({
|
||||
label: item.productName,
|
||||
value: item.id,
|
||||
}));
|
||||
const placeholder = productOptions.value.length > 0 ? '请选择' : '暂无产品';
|
||||
tableApi.formApi.updateSchema([
|
||||
{
|
||||
componentProps: {
|
||||
options: productOptions.value || [],
|
||||
placeholder,
|
||||
},
|
||||
fieldName: 'productId',
|
||||
},
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('获取产品列表失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
getProductOptionList();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page :auto-content-height="true">
|
||||
<BasicTable table-title="设备列表">
|
||||
<template #toolbar-tools>
|
||||
<Space>
|
||||
<a-button v-access:code="['device:device:export']" @click="handleDownloadExcel">
|
||||
{{ $t('pages.common.export') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
:disabled="!vxeCheckboxChecked(tableApi)"
|
||||
danger
|
||||
type="primary"
|
||||
v-access:code="['device:device:remove']"
|
||||
@click="handleMultiDelete"
|
||||
>
|
||||
{{ $t('pages.common.delete') }}
|
||||
</a-button>
|
||||
<a-button type="primary" v-access:code="['device:device:add']" @click="handleAdd">
|
||||
{{ $t('pages.common.add') }}
|
||||
</a-button>
|
||||
</Space>
|
||||
</template>
|
||||
<template #deviceType="{ row }">
|
||||
<Tag color="processing">
|
||||
{{ deviceTypeOptions.find((option) => option.value === row.deviceType)?.label }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template #deviceState="{ row }">
|
||||
<Tag :color="deviceStateOptions.find((option) => option.value === row.deviceState)?.type">
|
||||
{{ deviceStateOptions.find((option) => option.value === row.deviceState)?.label }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<Space>
|
||||
<ghost-button v-access:code="['device:device:view']" @click.stop="handleView(row)"> 详情 </ghost-button>
|
||||
<ghost-button v-access:code="['device:device:edit']" @click.stop="handleEdit(row)">
|
||||
{{ $t('pages.common.edit') }}
|
||||
</ghost-button>
|
||||
<Popconfirm
|
||||
:get-popup-container="getVxePopupContainer"
|
||||
placement="left"
|
||||
title="确认删除?"
|
||||
@confirm="handleDelete(row)"
|
||||
>
|
||||
<ghost-button danger v-access:code="['device:device:remove']" @click.stop="">
|
||||
{{ $t('pages.common.delete') }}
|
||||
</ghost-button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
</template>
|
||||
</BasicTable>
|
||||
<DeviceDrawer @reload="tableApi.query()" />
|
||||
</Page>
|
||||
</template>
|
|
@ -9,7 +9,7 @@ export const querySchema: FormSchemaGetter = () => [
|
|||
{
|
||||
component: 'Input',
|
||||
fieldName: 'productKey',
|
||||
label: '产品编码',
|
||||
label: '产品KEY',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
|
@ -51,7 +51,7 @@ export const columns: VxeGridProps['columns'] = [
|
|||
// field: 'id',
|
||||
// },
|
||||
{
|
||||
title: '产品编码',
|
||||
title: '产品KEY',
|
||||
field: 'productKey',
|
||||
},
|
||||
{
|
||||
|
@ -96,7 +96,7 @@ export const drawerSchema: FormSchemaGetter = () => [
|
|||
},
|
||||
},
|
||||
{
|
||||
label: '产品编码',
|
||||
label: '产品KEY',
|
||||
fieldName: 'productKey',
|
||||
component: 'Input',
|
||||
},
|
||||
|
@ -140,8 +140,7 @@ export const drawerSchema: FormSchemaGetter = () => [
|
|||
'span',
|
||||
{
|
||||
class: 'text-[14px] text-black/25 truncate',
|
||||
style:
|
||||
'max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;',
|
||||
style: 'max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;',
|
||||
},
|
||||
option.tooltip,
|
||||
),
|
||||
|
|
|
@ -18,7 +18,7 @@ import {
|
|||
} from 'ant-design-vue';
|
||||
|
||||
import { gatewayList } from '#/api/operations/gateway';
|
||||
import { enabledOptions, networkTypeOptions } from '#/constants/dicts';
|
||||
import { networkTypeOptions } from '#/constants/dicts';
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: [];
|
||||
|
@ -32,7 +32,6 @@ const selectedGateway = ref<any>(null);
|
|||
const searchForm = ref({
|
||||
name: '',
|
||||
provider: undefined,
|
||||
enabled: undefined,
|
||||
});
|
||||
|
||||
// 分页配置
|
||||
|
@ -42,8 +41,7 @@ const pagination = reactive({
|
|||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total: number, range: [number, number]) =>
|
||||
`第 ${range[0]}-${range[1]}条/总共${total}条`,
|
||||
showTotal: (total: number, range: [number, number]) => `第 ${range[0]}-${range[1]}条/总共${total}条`,
|
||||
});
|
||||
|
||||
// 网关列表数据
|
||||
|
@ -57,8 +55,7 @@ const providerOptions = [
|
|||
value: 'child-device',
|
||||
channel: 'child-device',
|
||||
transport: 'Gateway',
|
||||
description:
|
||||
'需要通过网关与平台进行数据通信的设备,将作为网关子设备接入到平台。',
|
||||
description: '需要通过网关与平台进行数据通信的设备,将作为网关子设备接入到平台。',
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -69,6 +66,7 @@ const loadGatewayList = async () => {
|
|||
const response = await gatewayList({
|
||||
pageNum: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
enabled: '1',
|
||||
...searchForm.value,
|
||||
});
|
||||
|
||||
|
@ -91,7 +89,6 @@ const loadGatewayList = async () => {
|
|||
const resetSearch = () => {
|
||||
searchForm.value.name = '';
|
||||
searchForm.value.provider = undefined;
|
||||
searchForm.value.enabled = undefined;
|
||||
pagination.current = 1;
|
||||
loadGatewayList();
|
||||
};
|
||||
|
@ -199,16 +196,6 @@ onMounted(() => {
|
|||
@change="(value) => (searchForm.provider = value)"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="状态">
|
||||
<Select
|
||||
v-model="searchForm.enabled"
|
||||
placeholder="请选择"
|
||||
allow-clear
|
||||
style="width: 140px"
|
||||
:options="enabledOptions"
|
||||
@change="(value) => (searchForm.enabled = value)"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<Space>
|
||||
<Button @click="resetSearch">重置</Button>
|
||||
|
@ -243,10 +230,7 @@ onMounted(() => {
|
|||
{{ getProviderText(gateway.provider) }}
|
||||
</Tag>
|
||||
</div>
|
||||
<Tag
|
||||
class="gateway-status"
|
||||
:color="getStatusColor(gateway.enabled)"
|
||||
>
|
||||
<Tag class="gateway-status" :color="getStatusColor(gateway.enabled)">
|
||||
{{ getStatusText(gateway.enabled) }}
|
||||
</Tag>
|
||||
<div class="gateway-footer">
|
||||
|
|
|
@ -69,8 +69,7 @@ const accessConfigs = ref([]);
|
|||
// 表格列配置
|
||||
const tableColumns = computed(() => {
|
||||
const isMQTT =
|
||||
accessInfo.value.provider === 'mqtt-server-gateway' ||
|
||||
accessInfo.value.provider === 'mqtt-client-gateway';
|
||||
accessInfo.value.provider === 'mqtt-server-gateway' || accessInfo.value.provider === 'mqtt-client-gateway';
|
||||
|
||||
return isMQTT
|
||||
? [
|
||||
|
@ -143,6 +142,7 @@ const handleSave = async () => {
|
|||
id: props.productInfo.id,
|
||||
provider: accessInfo.value.provider,
|
||||
gatewayId: accessInfo.value.id,
|
||||
protocolId: accessInfo.value.protocol,
|
||||
storePolicy: selectedStorePolicy.value,
|
||||
protocolConf: JSON.stringify(formData),
|
||||
});
|
||||
|
@ -254,8 +254,8 @@ onMounted(() => {
|
|||
</div>
|
||||
<div class="section-content">
|
||||
<p>{{ accessInfo.protocolName }}</p>
|
||||
<!-- <div v-if="accessInfo.document" v-html="markdownToHtml"></div>-->
|
||||
<div>{{markdownToHtml}}</div>
|
||||
<!-- <div v-if="accessInfo.document" v-html="markdownToHtml"></div>-->
|
||||
<div>{{ markdownToHtml }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -265,18 +265,9 @@ onMounted(() => {
|
|||
<h4>连接信息</h4>
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div
|
||||
v-if="accessInfo.addresses && accessInfo.addresses.length > 0"
|
||||
>
|
||||
<div
|
||||
v-for="item in accessInfo.addresses"
|
||||
:key="item.address"
|
||||
class="address-item"
|
||||
>
|
||||
<Badge
|
||||
:color="item.health === -1 ? 'red' : 'green'"
|
||||
:text="item.address"
|
||||
/>
|
||||
<div v-if="accessInfo.addresses && accessInfo.addresses.length > 0">
|
||||
<div v-for="item in accessInfo.addresses" :key="item.address" class="address-item">
|
||||
<Badge :color="item.health === -1 ? 'red' : 'green'" :text="item.address" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-address">暂无连接信息</div>
|
||||
|
@ -306,11 +297,7 @@ onMounted(() => {
|
|||
</div> -->
|
||||
|
||||
<!-- 其它接入配置 -->
|
||||
<div
|
||||
v-for="(config, index) in accessConfigs"
|
||||
:key="index"
|
||||
class="config-section"
|
||||
>
|
||||
<div v-for="(config, index) in accessConfigs" :key="index" class="config-section">
|
||||
<div class="section-header">
|
||||
<h4>{{ config.name }}</h4>
|
||||
<Tooltip title="此配置来自于产品接入网关所选择的协议">
|
||||
|
@ -327,11 +314,7 @@ onMounted(() => {
|
|||
:rules="[
|
||||
{
|
||||
required: !!item?.type?.expands?.required,
|
||||
message: `${
|
||||
item.type.type === 'enum' || 'boolean'
|
||||
? '请选择'
|
||||
: '请输入'
|
||||
}${item.name}`,
|
||||
message: `${item.type.type === 'enum' || 'boolean' ? '请选择' : '请输入'}${item.name}`,
|
||||
},
|
||||
]"
|
||||
>
|
||||
|
@ -346,19 +329,13 @@ onMounted(() => {
|
|||
placeholder="请输入"
|
||||
/>
|
||||
<Select
|
||||
v-if="
|
||||
item.type.type === 'enum' || item.type.type === 'boolean'
|
||||
"
|
||||
v-if="item.type.type === 'enum' || item.type.type === 'boolean'"
|
||||
v-model:value="formData[item.property]"
|
||||
placeholder="请选择"
|
||||
:options="getOptions(item)"
|
||||
/>
|
||||
<InputNumber
|
||||
v-if="
|
||||
['int', 'float', 'double', 'long'].includes(
|
||||
item.type.type,
|
||||
)
|
||||
"
|
||||
v-if="['int', 'float', 'double', 'long'].includes(item.type.type)"
|
||||
v-model:value="formData[item.property]"
|
||||
placeholder="请输入"
|
||||
/>
|
||||
|
@ -371,9 +348,7 @@ onMounted(() => {
|
|||
<div class="config-section">
|
||||
<div class="section-header">
|
||||
<h4>存储策略</h4>
|
||||
<Tooltip
|
||||
title="若修改存储策略,需要手动做数据迁移,平台只能搜索最新存储策略中的数据"
|
||||
>
|
||||
<Tooltip title="若修改存储策略,需要手动做数据迁移,平台只能搜索最新存储策略中的数据">
|
||||
<QuestionCircleOutlined />
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
@ -389,27 +364,18 @@ onMounted(() => {
|
|||
|
||||
<!-- 保存按钮 -->
|
||||
<div class="action-buttons">
|
||||
<a-button
|
||||
type="primary"
|
||||
@click="handleSave"
|
||||
:loading="saveLoading"
|
||||
v-access:code="['device:product:edit']"
|
||||
>
|
||||
<a-button type="primary" @click="handleSave" :loading="saveLoading" v-access:code="['device:product:edit']">
|
||||
保存
|
||||
</a-button>
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
<!-- 右侧信息展示 -->
|
||||
<Col
|
||||
:span="12"
|
||||
v-if="accessInfo.routes && accessInfo.routes.length > 0"
|
||||
>
|
||||
<Col :span="12" v-if="accessInfo.routes && accessInfo.routes.length > 0">
|
||||
<div class="info-panel">
|
||||
<h4>
|
||||
{{
|
||||
accessInfo.provider === 'mqtt-server-gateway' ||
|
||||
accessInfo.provider === 'mqtt-client-gateway'
|
||||
accessInfo.provider === 'mqtt-server-gateway' || accessInfo.provider === 'mqtt-client-gateway'
|
||||
? 'Topic信息'
|
||||
: 'URL信息'
|
||||
}}
|
||||
|
@ -461,11 +427,7 @@ onMounted(() => {
|
|||
@cancel="handleAccessModalClose"
|
||||
:footer="null"
|
||||
>
|
||||
<AccessSelector
|
||||
:product-id="productInfo.id"
|
||||
@select="handleModalSelect"
|
||||
@close="handleAccessModalClose"
|
||||
/>
|
||||
<AccessSelector :product-id="productInfo.id" @select="handleModalSelect" @close="handleAccessModalClose" />
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -163,6 +163,7 @@ const handleSave = async () => {
|
|||
emit('save', { ...formData.value });
|
||||
message.success('保存成功');
|
||||
visible.value = false;
|
||||
resetForm();
|
||||
} catch {
|
||||
message.error('保存失败');
|
||||
} finally {
|
||||
|
@ -196,12 +197,7 @@ watch(
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<Drawer
|
||||
v-model:open="visible"
|
||||
:title="drawerTitle"
|
||||
width="800px"
|
||||
@close="handleDrawerClose"
|
||||
>
|
||||
<Drawer v-model:open="visible" :title="drawerTitle" width="800px" @close="handleDrawerClose">
|
||||
<Form ref="formRef" :model="formData" :rules="formRules" layout="vertical">
|
||||
<Row :gutter="24">
|
||||
<Col :span="12">
|
||||
|
@ -220,18 +216,10 @@ watch(
|
|||
<Col :span="24">
|
||||
<FormItem label="输出参数" name="outputs">
|
||||
<div class="parameter-preview">
|
||||
<Table
|
||||
:columns="outputColumns"
|
||||
:data-source="formData.outputs"
|
||||
:pagination="false"
|
||||
>
|
||||
<Table :columns="outputColumns" :data-source="formData.outputs" :pagination="false">
|
||||
<template #bodyCell="{ column, record, index }">
|
||||
<template v-if="column.key === 'dataType'">
|
||||
{{
|
||||
dataTypeOptions.find(
|
||||
(item) => item.value === record.valueParams.dataType,
|
||||
)?.label
|
||||
}}
|
||||
{{ dataTypeOptions.find((item) => item.value === record.valueParams.dataType)?.label }}
|
||||
</template>
|
||||
<template v-if="column.key === 'required'">
|
||||
<Tag color="processing">
|
||||
|
@ -239,26 +227,12 @@ watch(
|
|||
</Tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'formType'">
|
||||
{{
|
||||
formTypeOptions.find(
|
||||
(item) => item.value === record.valueParams.formType,
|
||||
)?.label
|
||||
}}
|
||||
{{ formTypeOptions.find((item) => item.value === record.valueParams.formType)?.label }}
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<Space>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
@click="handleEditOutputParam(record, index)"
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
placement="left"
|
||||
title="确认删除?"
|
||||
@confirm="handleDeleteOutputParam(index)"
|
||||
>
|
||||
<Button type="link" size="small" @click="handleEditOutputParam(record, index)"> 编辑 </Button>
|
||||
<Popconfirm placement="left" title="确认删除?" @confirm="handleDeleteOutputParam(index)">
|
||||
<Button type="link" size="small" danger> 删除 </Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
|
@ -266,11 +240,7 @@ watch(
|
|||
</template>
|
||||
</Table>
|
||||
<div class="add-button-container">
|
||||
<a-button
|
||||
@click="handleAddOutputParam"
|
||||
type="primary"
|
||||
class="add-button"
|
||||
>
|
||||
<a-button @click="handleAddOutputParam" type="primary" class="add-button">
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
|
@ -283,21 +253,13 @@ watch(
|
|||
|
||||
<Col :span="12">
|
||||
<FormItem label="排序" name="sort">
|
||||
<InputNumber
|
||||
style="width: 100%"
|
||||
v-model:value="formData.sort"
|
||||
placeholder="请输入排序"
|
||||
/>
|
||||
<InputNumber style="width: 100%" v-model:value="formData.sort" placeholder="请输入排序" />
|
||||
</FormItem>
|
||||
</Col>
|
||||
|
||||
<Col :span="24">
|
||||
<FormItem label="描述" name="description">
|
||||
<Textarea
|
||||
v-model:value="formData.description"
|
||||
placeholder="请输入描述"
|
||||
:rows="3"
|
||||
/>
|
||||
<Textarea v-model:value="formData.description" placeholder="请输入描述" :rows="3" />
|
||||
</FormItem>
|
||||
</Col>
|
||||
</Row>
|
||||
|
@ -306,9 +268,7 @@ watch(
|
|||
<template #footer>
|
||||
<Space>
|
||||
<Button @click="handleDrawerClose">取消</Button>
|
||||
<Button type="primary" @click="handleSave" :loading="saveLoading">
|
||||
确认
|
||||
</Button>
|
||||
<Button type="primary" @click="handleSave" :loading="saveLoading"> 确认 </Button>
|
||||
</Space>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -204,6 +204,7 @@ const handleSave = async () => {
|
|||
|
||||
emit('save', { ...formData.value });
|
||||
visible.value = false;
|
||||
resetForm();
|
||||
} catch {
|
||||
message.error('保存失败');
|
||||
} finally {
|
||||
|
@ -219,10 +220,7 @@ const handleDrawerClose = () => {
|
|||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
Object.assign(
|
||||
formData.value,
|
||||
JSON.parse(JSON.stringify(defaultFunctionData)),
|
||||
);
|
||||
Object.assign(formData.value, JSON.parse(JSON.stringify(defaultFunctionData)));
|
||||
};
|
||||
|
||||
// 监听数据变化
|
||||
|
@ -240,12 +238,7 @@ watch(
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<Drawer
|
||||
v-model:open="visible"
|
||||
:title="drawerTitle"
|
||||
width="800px"
|
||||
@close="handleDrawerClose"
|
||||
>
|
||||
<Drawer v-model:open="visible" :title="drawerTitle" width="800px" @close="handleDrawerClose">
|
||||
<Form ref="formRef" :model="formData" :rules="formRules" layout="vertical">
|
||||
<Row :gutter="24">
|
||||
<Col :span="12">
|
||||
|
@ -262,18 +255,10 @@ watch(
|
|||
<Col :span="24">
|
||||
<FormItem label="输入参数" name="inputs">
|
||||
<div class="parameter-preview">
|
||||
<Table
|
||||
:columns="inputColumns"
|
||||
:data-source="formData.inputs"
|
||||
:pagination="false"
|
||||
>
|
||||
<Table :columns="inputColumns" :data-source="formData.inputs" :pagination="false">
|
||||
<template #bodyCell="{ column, record, index }">
|
||||
<template v-if="column.key === 'dataType'">
|
||||
{{
|
||||
dataTypeOptions.find(
|
||||
(item) => item.value === record.valueParams.dataType,
|
||||
)?.label
|
||||
}}
|
||||
{{ dataTypeOptions.find((item) => item.value === record.valueParams.dataType)?.label }}
|
||||
</template>
|
||||
<template v-if="column.key === 'required'">
|
||||
<Tag color="processing">
|
||||
|
@ -281,26 +266,12 @@ watch(
|
|||
</Tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'formType'">
|
||||
{{
|
||||
formTypeOptions.find(
|
||||
(item) => item.value === record.valueParams.formType,
|
||||
)?.label
|
||||
}}
|
||||
{{ formTypeOptions.find((item) => item.value === record.valueParams.formType)?.label }}
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<Space>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
@click="handleEditInputParam(record, index)"
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
placement="left"
|
||||
title="确认删除?"
|
||||
@confirm="handleDeleteInputParam(index)"
|
||||
>
|
||||
<Button type="link" size="small" @click="handleEditInputParam(record, index)"> 编辑 </Button>
|
||||
<Popconfirm placement="left" title="确认删除?" @confirm="handleDeleteInputParam(index)">
|
||||
<Button type="link" size="small" danger> 删除 </Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
|
@ -308,11 +279,7 @@ watch(
|
|||
</template>
|
||||
</Table>
|
||||
<div class="add-button-container">
|
||||
<a-button
|
||||
@click="handleAddInputParam"
|
||||
type="primary"
|
||||
class="add-button"
|
||||
>
|
||||
<a-button @click="handleAddInputParam" type="primary" class="add-button">
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
|
@ -327,11 +294,7 @@ watch(
|
|||
<Col :span="12">
|
||||
<FormItem label="是否异步" name="async">
|
||||
<RadioGroup v-model:value="formData.async" button-style="solid">
|
||||
<RadioButton
|
||||
:value="item.value"
|
||||
v-for="item in asyncOptions"
|
||||
:key="item.value"
|
||||
>
|
||||
<RadioButton :value="item.value" v-for="item in asyncOptions" :key="item.value">
|
||||
{{ item.label }}
|
||||
</RadioButton>
|
||||
</RadioGroup>
|
||||
|
@ -340,15 +303,8 @@ watch(
|
|||
|
||||
<Col :span="12">
|
||||
<FormItem label="是否开启前置校验" name="expands.preCheck">
|
||||
<RadioGroup
|
||||
v-model:value="formData.expands.preCheck"
|
||||
button-style="solid"
|
||||
>
|
||||
<RadioButton
|
||||
:value="item.value"
|
||||
v-for="item in preCheckOptions"
|
||||
:key="item.value"
|
||||
>
|
||||
<RadioGroup v-model:value="formData.expands.preCheck" button-style="solid">
|
||||
<RadioButton :value="item.value" v-for="item in preCheckOptions" :key="item.value">
|
||||
{{ item.label }}
|
||||
</RadioButton>
|
||||
</RadioGroup>
|
||||
|
@ -375,21 +331,13 @@ watch(
|
|||
|
||||
<Col :span="12">
|
||||
<FormItem label="排序" name="sort">
|
||||
<InputNumber
|
||||
style="width: 100%"
|
||||
v-model:value="formData.sort"
|
||||
placeholder="请输入排序"
|
||||
/>
|
||||
<InputNumber style="width: 100%" v-model:value="formData.sort" placeholder="请输入排序" />
|
||||
</FormItem>
|
||||
</Col>
|
||||
|
||||
<Col :span="24">
|
||||
<FormItem label="描述" name="description">
|
||||
<Textarea
|
||||
v-model:value="formData.description"
|
||||
placeholder="请输入描述"
|
||||
:rows="3"
|
||||
/>
|
||||
<Textarea v-model:value="formData.description" placeholder="请输入描述" :rows="3" />
|
||||
</FormItem>
|
||||
</Col>
|
||||
</Row>
|
||||
|
@ -398,9 +346,7 @@ watch(
|
|||
<template #footer>
|
||||
<Space>
|
||||
<Button @click="handleDrawerClose">取消</Button>
|
||||
<Button type="primary" @click="handleSave" :loading="saveLoading">
|
||||
保存
|
||||
</Button>
|
||||
<Button type="primary" @click="handleSave" :loading="saveLoading"> 确认 </Button>
|
||||
</Space>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,15 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Drawer,
|
||||
message,
|
||||
Modal,
|
||||
Space,
|
||||
TabPane,
|
||||
Tabs,
|
||||
} from 'ant-design-vue';
|
||||
import { Button, Drawer, message, Modal, Space, TabPane, Tabs } from 'ant-design-vue';
|
||||
|
||||
import { productUpdateById } from '#/api/device/product';
|
||||
|
||||
|
@ -143,7 +135,7 @@ const loadMetadata = async () => {
|
|||
// objectConf: [],
|
||||
// unit: '',
|
||||
// scale: 2,
|
||||
// format: 'yyyy-MM-dd HH:mm:ss',
|
||||
// format: 'YYYY-MM-DD HH:mm:ss',
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
|
@ -171,7 +163,7 @@ const loadMetadata = async () => {
|
|||
// objectConf: [],
|
||||
// unit: '',
|
||||
// scale: 2,
|
||||
// format: 'yyyy-MM-dd HH:mm:ss',
|
||||
// format: 'YYYY-MM-DD HH:mm:ss',
|
||||
// },
|
||||
// expands: {},
|
||||
// },
|
||||
|
@ -197,7 +189,7 @@ const loadMetadata = async () => {
|
|||
// objectConf: [],
|
||||
// unit: '',
|
||||
// scale: 2,
|
||||
// format: 'yyyy-MM-dd HH:mm:ss',
|
||||
// format: 'YYYY-MM-DD HH:mm:ss',
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
|
@ -216,15 +208,44 @@ const loadMetadata = async () => {
|
|||
}
|
||||
};
|
||||
|
||||
// 递归移除 _pk
|
||||
const stripPkDeep = (data: any): any => {
|
||||
if (Array.isArray(data)) return data.map((item) => stripPkDeep(item));
|
||||
if (data && typeof data === 'object') {
|
||||
const { _pk, ...rest } = data as Record<string, any>;
|
||||
Object.keys(rest).forEach((k) => (rest[k] = stripPkDeep(rest[k])));
|
||||
return rest;
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
// 保存物模型
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
// 这里调用保存API
|
||||
const metadata = JSON.parse(JSON.stringify(currentMetadata.value));
|
||||
metadata.properties.sort((a: any, b: any) => a.sort - b.sort);
|
||||
metadata.functions.sort((a: any, b: any) => a.sort - b.sort);
|
||||
metadata.events.sort((a: any, b: any) => a.sort - b.sort);
|
||||
metadata.propertyGroups.sort((a: any, b: any) => a.sort - b.sort); // 新增属性分组排序
|
||||
|
||||
// 移除所有 _pk,并排序
|
||||
metadata.properties = (metadata.properties || [])
|
||||
.map((x: any) => stripPkDeep(x))
|
||||
.sort((a: any, b: any) => (a.sort ?? 0) - (b.sort ?? 0));
|
||||
|
||||
metadata.functions = (metadata.functions || [])
|
||||
.map((x: any) => stripPkDeep(x))
|
||||
.sort((a: any, b: any) => (a.sort ?? 0) - (b.sort ?? 0));
|
||||
|
||||
metadata.events = (metadata.events || [])
|
||||
.map((x: any) => stripPkDeep(x))
|
||||
.sort((a: any, b: any) => (a.sort ?? 0) - (b.sort ?? 0));
|
||||
|
||||
metadata.propertyGroups = (metadata.propertyGroups || [])
|
||||
.map((g: any) => {
|
||||
const next = stripPkDeep(g);
|
||||
next.properties = (next.properties || []).map((item) => stripPkDeep(item));
|
||||
return next;
|
||||
})
|
||||
.sort((a: any, b: any) => (a.sort ?? 0) - (b.sort ?? 0));
|
||||
|
||||
await productUpdateById(props.productInfo.id, {
|
||||
id: props.productInfo.id,
|
||||
metadata: JSON.stringify(metadata),
|
||||
|
@ -285,9 +306,7 @@ watch(
|
|||
currentMetadata,
|
||||
() => {
|
||||
if (originalMetadata.value) {
|
||||
metadataChanged.value =
|
||||
JSON.stringify(currentMetadata.value) !==
|
||||
JSON.stringify(originalMetadata.value);
|
||||
metadataChanged.value = JSON.stringify(currentMetadata.value) !== JSON.stringify(originalMetadata.value);
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
|
@ -302,36 +321,16 @@ loadMetadata();
|
|||
<div class="metadata-header">
|
||||
<div class="header-left">
|
||||
<h3>物模型</h3>
|
||||
<span class="desc">
|
||||
设备会默认继承产品的物模型,继承的物模型不支持删改
|
||||
</span>
|
||||
<span class="desc"> 设备会默认继承产品的物模型,继承的物模型不支持删改 </span>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<Space>
|
||||
<Button
|
||||
v-if="metadataChanged"
|
||||
type="primary"
|
||||
@click="handleSave"
|
||||
v-access:code="['device:product:edit']"
|
||||
>
|
||||
<Button v-if="metadataChanged" type="primary" @click="handleSave" v-access:code="['device:product:edit']">
|
||||
保存
|
||||
</Button>
|
||||
<Button
|
||||
v-if="showReset"
|
||||
@click="handleReset"
|
||||
v-access:code="['device:product:edit']"
|
||||
>
|
||||
重置操作
|
||||
</Button>
|
||||
<Button @click="handleImport" v-access:code="['device:product:edit']">
|
||||
快速导入
|
||||
</Button>
|
||||
<Button
|
||||
@click="handleViewTSL"
|
||||
v-access:code="['device:product:edit']"
|
||||
>
|
||||
物模型
|
||||
</Button>
|
||||
<Button v-if="showReset" @click="handleReset" v-access:code="['device:product:edit']"> 重置操作 </Button>
|
||||
<Button @click="handleImport" v-access:code="['device:product:edit']"> 快速导入 </Button>
|
||||
<Button @click="handleViewTSL" v-access:code="['device:product:edit']"> 物模型 </Button>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -377,25 +376,16 @@ loadMetadata();
|
|||
</Tabs>
|
||||
|
||||
<!-- 导入抽屉 -->
|
||||
<Drawer
|
||||
v-model:open="importVisible"
|
||||
title="快速导入"
|
||||
width="600px"
|
||||
@close="handleImportClose"
|
||||
>
|
||||
<Drawer v-model:open="importVisible" title="快速导入" width="600px" @close="handleImportClose">
|
||||
<ImportForm @success="handleImportSuccess" />
|
||||
</Drawer>
|
||||
|
||||
<!-- TSL查看抽屉 -->
|
||||
<Drawer
|
||||
v-model:open="tslVisible"
|
||||
title="物模型"
|
||||
width="800px"
|
||||
@close="handleTSLClose"
|
||||
>
|
||||
<Drawer v-model:open="tslVisible" title="物模型" width="800px" @close="handleTSLClose">
|
||||
<TSLViewer
|
||||
:product-id="productInfo.id"
|
||||
:product-info="productInfo"
|
||||
:file-name="productInfo.productName"
|
||||
:metadata="tslMetadata"
|
||||
/>
|
||||
</Drawer>
|
||||
|
|
|
@ -34,6 +34,26 @@ const tableData = ref<any[]>([]);
|
|||
|
||||
const formData = ref<any>(null);
|
||||
|
||||
// 生成稳定主键
|
||||
const generatePk = (): string => `${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
||||
|
||||
// 为记录补齐主键
|
||||
const withPk = (record: any) => {
|
||||
if (!record) return record;
|
||||
if (!record._pk) {
|
||||
Object.defineProperty(record, '_pk', {
|
||||
value: generatePk(),
|
||||
enumerable: true, // 需要在表格 rowKey 中可见
|
||||
configurable: true,
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
return record;
|
||||
};
|
||||
|
||||
// 通过主键查找索引
|
||||
const findIndexByPk = (pk: string) => tableData.value.findIndex((item) => item._pk === pk);
|
||||
|
||||
// 表格列配置
|
||||
const columns = computed(() => {
|
||||
const baseColumns = [
|
||||
|
@ -148,12 +168,23 @@ const getTypeLabel = () => {
|
|||
return labels[props.type];
|
||||
};
|
||||
|
||||
// 对表数据进行稳定排序
|
||||
const sortTable = () => {
|
||||
tableData.value = [...tableData.value].sort((a: any, b: any) => {
|
||||
const as = Number(a?.sort ?? 0);
|
||||
const bs = Number(b?.sort ?? 0);
|
||||
if (as !== bs) return as - bs;
|
||||
return String(a?._pk ?? '').localeCompare(String(b?._pk ?? ''));
|
||||
});
|
||||
};
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
// 使用传入的metadata数据
|
||||
tableData.value = [...(props.metadata || [])];
|
||||
// 使用传入的metadata数据,并补齐 _pk
|
||||
tableData.value = (props.metadata || []).map((item) => withPk({ ...item }));
|
||||
sortTable();
|
||||
} catch {
|
||||
message.error('加载数据失败');
|
||||
} finally {
|
||||
|
@ -169,9 +200,11 @@ const handleAdd = () => {
|
|||
};
|
||||
|
||||
// 编辑
|
||||
const editingPk = ref<null | string>(null);
|
||||
const handleEdit = (record: any, index: number) => {
|
||||
editDataIndex.value = index;
|
||||
editDataIndex.value = index; // 仅用于显示,不再依赖该索引
|
||||
isEdit.value = true;
|
||||
editingPk.value = record._pk;
|
||||
formData.value = JSON.parse(JSON.stringify(record));
|
||||
drawerVisible.value = true;
|
||||
};
|
||||
|
@ -179,10 +212,10 @@ const handleEdit = (record: any, index: number) => {
|
|||
// 删除
|
||||
const handleDelete = async (record: any) => {
|
||||
try {
|
||||
// 本地删除
|
||||
const index = tableData.value.findIndex((item) => item.id === record.id);
|
||||
if (index !== -1) {
|
||||
tableData.value.splice(index, 1);
|
||||
const idx = findIndexByPk(record._pk);
|
||||
if (idx !== -1) {
|
||||
tableData.value.splice(idx, 1);
|
||||
sortTable();
|
||||
emit('change', tableData.value);
|
||||
message.success('删除成功');
|
||||
}
|
||||
|
@ -195,17 +228,21 @@ const handleDelete = async (record: any) => {
|
|||
const handleSave = async (data: any) => {
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
// 编辑模式
|
||||
if (editDataIndex.value === -1) {
|
||||
tableData.value.push({ ...data });
|
||||
// 编辑模式:根据 _pk 替换,并保持 _pk 不变
|
||||
const idx = editingPk.value ? findIndexByPk(editingPk.value) : -1;
|
||||
const next = { ...data };
|
||||
if (idx === -1) {
|
||||
tableData.value.push(withPk(next));
|
||||
} else {
|
||||
tableData.value[editDataIndex.value] = { ...data };
|
||||
next._pk = tableData.value[idx]._pk;
|
||||
tableData.value[idx] = next;
|
||||
}
|
||||
} else {
|
||||
// 新增模式
|
||||
tableData.value.push({ ...data });
|
||||
tableData.value.push(withPk({ ...data }));
|
||||
}
|
||||
|
||||
sortTable();
|
||||
drawerVisible.value = false;
|
||||
emit('change', tableData.value);
|
||||
} catch {
|
||||
|
@ -216,6 +253,7 @@ const handleSave = async (data: any) => {
|
|||
// 重置表单
|
||||
const resetForm = () => {
|
||||
formData.value = null;
|
||||
editingPk.value = null;
|
||||
};
|
||||
|
||||
// 监听metadata变化
|
||||
|
@ -235,11 +273,7 @@ onMounted(() => {
|
|||
<template>
|
||||
<div class="metadata-table">
|
||||
<div class="table-header">
|
||||
<Button
|
||||
type="primary"
|
||||
@click="handleAdd"
|
||||
v-access:code="['device:product:edit']"
|
||||
>
|
||||
<Button type="primary" @click="handleAdd" v-access:code="['device:product:edit']">
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
|
@ -247,52 +281,26 @@ onMounted(() => {
|
|||
</Button>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
:columns="columns"
|
||||
:data-source="tableData"
|
||||
:loading="loading"
|
||||
:pagination="false"
|
||||
row-key="id"
|
||||
>
|
||||
<Table :columns="columns" :data-source="tableData" :loading="loading" :pagination="false" row-key="_pk">
|
||||
<template #bodyCell="{ column, record, index }">
|
||||
<template v-if="column.key === 'action'">
|
||||
<Space>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
@click="handleEdit(record, index)"
|
||||
v-access:code="['device:product:edit']"
|
||||
>
|
||||
<Button type="link" size="small" @click="handleEdit(record, index)" v-access:code="['device:product:edit']">
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm title="确认删除?" @confirm="handleDelete(record)">
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
danger
|
||||
v-access:code="['device:product:edit']"
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
<Button type="link" size="small" danger v-access:code="['device:product:edit']"> 删除 </Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'dataType'">
|
||||
<Tag color="processing">
|
||||
{{
|
||||
dataTypeOptions.find(
|
||||
(item) => item.value === record.valueParams?.dataType,
|
||||
)?.label
|
||||
}}
|
||||
{{ dataTypeOptions.find((item) => item.value === record.valueParams?.dataType)?.label }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'expands.type'">
|
||||
<Tag color="processing">
|
||||
{{
|
||||
readWriteTypeOptions.find(
|
||||
(item) => item.value === record.expands.type,
|
||||
)?.label
|
||||
}}
|
||||
{{ readWriteTypeOptions.find((item) => item.value === record.expands.type)?.label }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'required'">
|
||||
|
|
|
@ -78,6 +78,55 @@ const visible = computed({
|
|||
set: (value) => emit('update:open', value),
|
||||
});
|
||||
|
||||
const filterFormTypeOptions = computed(() => {
|
||||
switch (formData?.value?.valueParams?.dataType) {
|
||||
case 'boolean': {
|
||||
return formTypeOptions.filter((item) => {
|
||||
return item.value === 'switch';
|
||||
});
|
||||
}
|
||||
case 'date': {
|
||||
return formTypeOptions.filter((item) => {
|
||||
return item.value === 'time';
|
||||
});
|
||||
}
|
||||
case 'double':
|
||||
case 'float':
|
||||
case 'int':
|
||||
case 'long': {
|
||||
return formTypeOptions.filter((item) => {
|
||||
return item.value === 'number' || item.value === 'progress';
|
||||
});
|
||||
}
|
||||
case 'enum': {
|
||||
return formTypeOptions.filter((item) => {
|
||||
return item.value === 'select';
|
||||
});
|
||||
}
|
||||
case 'string': {
|
||||
return formTypeOptions.filter((item) => {
|
||||
return item.value === 'input' || item.value === 'switch' || item.value === 'select' || item.value === 'time';
|
||||
});
|
||||
}
|
||||
default: {
|
||||
return formTypeOptions;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const filterTimeOptions = computed(() => {
|
||||
switch (formData?.value?.valueParams?.dataType) {
|
||||
case 'date': {
|
||||
return timeOptions.filter((item) => {
|
||||
return item.value === 'YYYY-MM-DD' || item.value === 'YYYY-MM-DD HH:mm:ss';
|
||||
});
|
||||
}
|
||||
default: {
|
||||
return timeOptions;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const formRef = ref();
|
||||
const saveLoading = ref(false);
|
||||
const enumModalVisible = ref(false);
|
||||
|
@ -127,12 +176,43 @@ const drawerTitle = computed(() => {
|
|||
return props.isEdit ? '编辑属性' : '新增属性';
|
||||
});
|
||||
|
||||
const handleDataTypeChange = (value: string) => {
|
||||
switch (value) {
|
||||
case 'boolean': {
|
||||
formData.value.valueParams.formType = 'switch';
|
||||
|
||||
break;
|
||||
}
|
||||
case 'date': {
|
||||
formData.value.valueParams.formType = 'time';
|
||||
|
||||
break;
|
||||
}
|
||||
case 'double':
|
||||
case 'float':
|
||||
case 'int':
|
||||
case 'long': {
|
||||
formData.value.valueParams.formType = 'number';
|
||||
|
||||
break;
|
||||
}
|
||||
case 'enum': {
|
||||
formData.value.valueParams.formType = 'select';
|
||||
|
||||
break;
|
||||
}
|
||||
case 'string': {
|
||||
formData.value.valueParams.formType = 'input';
|
||||
|
||||
break;
|
||||
}
|
||||
// No default
|
||||
}
|
||||
};
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
Object.assign(
|
||||
formData.value,
|
||||
JSON.parse(JSON.stringify(defaultPropertyData)),
|
||||
);
|
||||
Object.assign(formData.value, JSON.parse(JSON.stringify(defaultPropertyData)));
|
||||
};
|
||||
|
||||
// 打开枚举列表编辑弹窗
|
||||
|
@ -154,6 +234,7 @@ const handleSave = async () => {
|
|||
|
||||
emit('save', { ...formData.value });
|
||||
visible.value = false;
|
||||
resetForm();
|
||||
} catch {
|
||||
message.error('保存失败');
|
||||
} finally {
|
||||
|
@ -179,15 +260,22 @@ watch(
|
|||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => formData.value.valueParams.formType,
|
||||
(newVal) => {
|
||||
if (newVal === 'switch') {
|
||||
formData.value.valueParams.trueText = '是';
|
||||
formData.value.valueParams.trueValue = 'true';
|
||||
formData.value.valueParams.falseText = '否';
|
||||
formData.value.valueParams.falseValue = 'false';
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Drawer
|
||||
v-model:open="visible"
|
||||
:title="drawerTitle"
|
||||
width="800px"
|
||||
@close="handleDrawerClose"
|
||||
>
|
||||
<Drawer v-model:open="visible" :title="drawerTitle" width="800px" @close="handleDrawerClose">
|
||||
<Form ref="formRef" :model="formData" :rules="formRules" layout="vertical">
|
||||
<Row :gutter="24">
|
||||
<Col :span="12">
|
||||
|
@ -208,6 +296,7 @@ watch(
|
|||
v-model:value="formData.valueParams.dataType"
|
||||
placeholder="请选择数据类型"
|
||||
:options="dataTypeOptions"
|
||||
@change="handleDataTypeChange"
|
||||
/>
|
||||
</FormItem>
|
||||
</Col>
|
||||
|
@ -216,7 +305,7 @@ watch(
|
|||
<Select
|
||||
v-model:value="formData.valueParams.formType"
|
||||
placeholder="请选择表单类型"
|
||||
:options="formTypeOptions"
|
||||
:options="filterFormTypeOptions"
|
||||
/>
|
||||
</FormItem>
|
||||
</Col>
|
||||
|
@ -234,13 +323,7 @@ watch(
|
|||
<Col :span="24" v-if="formData.valueParams.formType === 'switch'">
|
||||
<Row
|
||||
:gutter="24"
|
||||
style="
|
||||
padding-top: 16px;
|
||||
margin: 0;
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 4px;
|
||||
"
|
||||
style="padding-top: 16px; margin: 0; margin-bottom: 16px; border: 1px solid #f0f0f0; border-radius: 4px"
|
||||
>
|
||||
<Col :span="12">
|
||||
<FormItem label="开关打开名称" name="valueParams.trueText">
|
||||
|
@ -285,14 +368,9 @@ watch(
|
|||
<Col :span="24" v-if="formData.valueParams.formType === 'select'">
|
||||
<FormItem label="枚举列表" name="valueParams.enumConf">
|
||||
<div class="enum-preview">
|
||||
<a-button type="primary" @click="handleEditEnum">
|
||||
编辑枚举列表
|
||||
</a-button>
|
||||
<a-button type="primary" @click="handleEditEnum"> 编辑枚举列表 </a-button>
|
||||
<Descriptions
|
||||
v-if="
|
||||
formData.valueParams.enumConf &&
|
||||
formData.valueParams.enumConf.length > 0
|
||||
"
|
||||
v-if="formData.valueParams.enumConf && formData.valueParams.enumConf.length > 0"
|
||||
:column="4"
|
||||
bordered
|
||||
title=" "
|
||||
|
@ -312,30 +390,30 @@ watch(
|
|||
<!-- 数字类型配置 -->
|
||||
<Col
|
||||
:span="12"
|
||||
v-if="
|
||||
formData.valueParams.formType === 'number' ||
|
||||
formData.valueParams.formType === 'progress'
|
||||
"
|
||||
v-if="formData.valueParams.formType === 'number' || formData.valueParams.formType === 'progress'"
|
||||
>
|
||||
<FormItem label="最小值" name="valueParams.min">
|
||||
<InputNumber
|
||||
style="width: 100%"
|
||||
v-model:value="formData.valueParams.min"
|
||||
:precision="
|
||||
formData.valueParams.dataType === 'double' || formData.valueParams.dataType === 'float' ? undefined : 0
|
||||
"
|
||||
placeholder="请输入最小值"
|
||||
/>
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col
|
||||
:span="12"
|
||||
v-if="
|
||||
formData.valueParams.formType === 'number' ||
|
||||
formData.valueParams.formType === 'progress'
|
||||
"
|
||||
v-if="formData.valueParams.formType === 'number' || formData.valueParams.formType === 'progress'"
|
||||
>
|
||||
<FormItem label="最大值" name="valueParams.max">
|
||||
<InputNumber
|
||||
style="width: 100%"
|
||||
v-model:value="formData.valueParams.max"
|
||||
:precision="
|
||||
formData.valueParams.dataType === 'double' || formData.valueParams.dataType === 'float' ? undefined : 0
|
||||
"
|
||||
placeholder="请输入最大值"
|
||||
/>
|
||||
</FormItem>
|
||||
|
@ -345,31 +423,19 @@ watch(
|
|||
<InputNumber
|
||||
style="width: 100%"
|
||||
v-model:value="formData.valueParams.length"
|
||||
:precision="0"
|
||||
placeholder="请输入长度"
|
||||
/>
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col
|
||||
:span="12"
|
||||
v-if="
|
||||
formData.valueParams.formType === 'number' ||
|
||||
formData.valueParams.formType === 'progress'
|
||||
"
|
||||
>
|
||||
<Col :span="12" v-if="formData.valueParams.dataType === 'double' || formData.valueParams.dataType === 'float'">
|
||||
<FormItem label="小数位" name="valueParams.scale">
|
||||
<InputNumber
|
||||
style="width: 100%"
|
||||
v-model:value="formData.valueParams.scale"
|
||||
placeholder="请输入小数位"
|
||||
/>
|
||||
<InputNumber style="width: 100%" v-model:value="formData.valueParams.scale" placeholder="请输入小数位" />
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="12">
|
||||
<FormItem label="单位" name="valueParams.unit">
|
||||
<Input
|
||||
v-model:value="formData.valueParams.unit"
|
||||
placeholder="请输入单位"
|
||||
/>
|
||||
<Input v-model:value="formData.valueParams.unit" placeholder="请输入单位" />
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="12" v-if="formData.valueParams.formType === 'time'">
|
||||
|
@ -377,7 +443,7 @@ watch(
|
|||
<Select
|
||||
v-model:value="formData.valueParams.format"
|
||||
placeholder="请选择时间格式"
|
||||
:options="timeOptions"
|
||||
:options="filterTimeOptions"
|
||||
/>
|
||||
</FormItem>
|
||||
</Col>
|
||||
|
@ -390,15 +456,8 @@ watch(
|
|||
</Col>
|
||||
<Col :span="12">
|
||||
<FormItem label="读写类型" name="type">
|
||||
<RadioGroup
|
||||
v-model:value="formData.expands.type"
|
||||
button-style="solid"
|
||||
>
|
||||
<RadioButton
|
||||
:value="item.value"
|
||||
v-for="item in readWriteTypeOptions"
|
||||
:key="item.value"
|
||||
>
|
||||
<RadioGroup v-model:value="formData.expands.type" button-style="solid">
|
||||
<RadioButton :value="item.value" v-for="item in readWriteTypeOptions" :key="item.value">
|
||||
{{ item.label }}
|
||||
</RadioButton>
|
||||
</RadioGroup>
|
||||
|
@ -407,21 +466,13 @@ watch(
|
|||
|
||||
<Col :span="12">
|
||||
<FormItem label="排序" name="sort">
|
||||
<InputNumber
|
||||
style="width: 100%"
|
||||
v-model:value="formData.sort"
|
||||
placeholder="请输入排序"
|
||||
/>
|
||||
<InputNumber style="width: 100%" v-model:value="formData.sort" placeholder="请输入排序" />
|
||||
</FormItem>
|
||||
</Col>
|
||||
|
||||
<Col :span="24">
|
||||
<FormItem label="描述" name="description">
|
||||
<Textarea
|
||||
v-model:value="formData.description"
|
||||
placeholder="请输入描述"
|
||||
:rows="3"
|
||||
/>
|
||||
<Textarea v-model:value="formData.description" placeholder="请输入描述" :rows="3" />
|
||||
</FormItem>
|
||||
</Col>
|
||||
</Row>
|
||||
|
@ -430,9 +481,7 @@ watch(
|
|||
<template #footer>
|
||||
<Space>
|
||||
<Button @click="handleDrawerClose">取消</Button>
|
||||
<Button type="primary" @click="handleSave" :loading="saveLoading">
|
||||
确认
|
||||
</Button>
|
||||
<Button type="primary" @click="handleSave" :loading="saveLoading"> 确认 </Button>
|
||||
</Space>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -140,9 +140,7 @@ const handleDeleteProperty = (propertyId: string) => {
|
|||
// 属性选择确认
|
||||
const handlePropertySelectionConfirm = (selectedProperties: any[]) => {
|
||||
// 过滤掉已经选择的属性
|
||||
const newProperties = selectedProperties.filter(
|
||||
(prop) => !formData.value.properties.some((p) => p.id === prop.id),
|
||||
);
|
||||
const newProperties = selectedProperties.filter((prop) => !formData.value.properties.some((p) => p.id === prop.id));
|
||||
|
||||
if (newProperties.length > 0) {
|
||||
formData.value.properties.push(...newProperties);
|
||||
|
@ -161,6 +159,8 @@ const handleSave = async () => {
|
|||
saveLoading.value = true;
|
||||
|
||||
emit('save', JSON.parse(JSON.stringify(formData.value)));
|
||||
visible.value = false;
|
||||
resetForm();
|
||||
} catch (error) {
|
||||
console.error('表单验证失败:', error);
|
||||
} finally {
|
||||
|
@ -176,12 +176,7 @@ const handleClose = () => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<Drawer
|
||||
v-model:open="visible"
|
||||
:title="isEdit ? '编辑分组' : '添加'"
|
||||
width="800px"
|
||||
@close="handleClose"
|
||||
>
|
||||
<Drawer v-model:open="visible" :title="isEdit ? '编辑分组' : '添加'" width="800px" @close="handleClose">
|
||||
<Form ref="formRef" :model="formData" :rules="formRules" layout="vertical">
|
||||
<Row :gutter="16">
|
||||
<Col :span="12">
|
||||
|
@ -207,23 +202,12 @@ const handleClose = () => {
|
|||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'action'">
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
danger
|
||||
@click="handleDeleteProperty(record.id)"
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
<Button type="link" size="small" danger @click="handleDeleteProperty(record.id)"> 删除 </Button>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
<div class="add-button-container">
|
||||
<a-button
|
||||
@click="handleAddProperty"
|
||||
type="primary"
|
||||
class="add-button"
|
||||
>
|
||||
<a-button @click="handleAddProperty" type="primary" class="add-button">
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
|
@ -234,21 +218,12 @@ const handleClose = () => {
|
|||
</Col>
|
||||
<Col :span="12">
|
||||
<FormItem label="排序" name="sort">
|
||||
<InputNumber
|
||||
v-model:value="formData.sort"
|
||||
:min="1"
|
||||
style="width: 100%"
|
||||
placeholder="请输入"
|
||||
/>
|
||||
<InputNumber v-model:value="formData.sort" :min="1" style="width: 100%" placeholder="请输入" />
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="24">
|
||||
<FormItem label="描述" name="description">
|
||||
<Textarea
|
||||
v-model:value="formData.description"
|
||||
placeholder="请输入"
|
||||
:rows="3"
|
||||
/>
|
||||
<Textarea v-model:value="formData.description" placeholder="请输入" :rows="3" />
|
||||
</FormItem>
|
||||
</Col>
|
||||
</Form>
|
||||
|
@ -256,9 +231,7 @@ const handleClose = () => {
|
|||
<template #footer>
|
||||
<Space>
|
||||
<Button @click="handleClose">取消</Button>
|
||||
<Button type="primary" :loading="saveLoading" @click="handleSave">
|
||||
确认
|
||||
</Button>
|
||||
<Button type="primary" :loading="saveLoading" @click="handleSave"> 确认 </Button>
|
||||
</Space>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,17 +1,14 @@
|
|||
<script setup lang="ts">
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
|
||||
import {
|
||||
CopyOutlined,
|
||||
DownloadOutlined,
|
||||
ReloadOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import { CopyOutlined, DownloadOutlined, ReloadOutlined } from '@ant-design/icons-vue';
|
||||
import { message, Space } from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import MonacoEditor from '#/components/MonacoEditor/index.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
fileName: string;
|
||||
metadata: object;
|
||||
productId: string;
|
||||
productInfo: object;
|
||||
|
@ -60,7 +57,7 @@ const handleRefresh = () => {
|
|||
// 导出
|
||||
const handleExport = () => {
|
||||
const time = dayjs().format('YYYY-MM-DD_HH:mm:ss');
|
||||
exportFileName.value = `${props.productInfo.productName}_物模型_${time}`;
|
||||
exportFileName.value = `${props.fileName}_物模型_${time}`;
|
||||
handleExportConfirm();
|
||||
};
|
||||
|
||||
|
@ -154,12 +151,7 @@ onMounted(() => {
|
|||
|
||||
<div class="viewer-content">
|
||||
<div class="viewer-toolbar">
|
||||
<MonacoEditor
|
||||
v-model="tslJson"
|
||||
lang="javascript"
|
||||
style="height: 100%"
|
||||
theme="vs"
|
||||
/>
|
||||
<MonacoEditor v-model="tslJson" lang="javascript" style="height: 100%" theme="vs" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -5,9 +5,9 @@ import { useRoute, useRouter } from 'vue-router';
|
|||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { ArrowLeftOutlined } from '@ant-design/icons-vue';
|
||||
import { message, Switch, TabPane, Tabs } from 'ant-design-vue';
|
||||
import { message, Modal, Switch, TabPane, Tabs } from 'ant-design-vue';
|
||||
|
||||
import { productUpdate, productUpdateStatus } from '#/api/device/product';
|
||||
import { productPushMetadataById, productUpdateStatus } from '#/api/device/product';
|
||||
import { useProductStore } from '#/store/product';
|
||||
|
||||
import BasicInfo from './components/BasicInfo.vue';
|
||||
|
@ -51,12 +51,18 @@ const handleStatusChange = async (checked: boolean) => {
|
|||
// 应用配置
|
||||
const handleApplyConfig = async () => {
|
||||
try {
|
||||
await productUpdate({
|
||||
id: productId.value,
|
||||
enabled: '1',
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确认要应用到产品下的所有设备吗?',
|
||||
onOk: async () => {
|
||||
await productPushMetadataById(productId.value);
|
||||
await loadProductInfo();
|
||||
},
|
||||
});
|
||||
message.success('配置应用成功');
|
||||
await loadProductInfo();
|
||||
// await productPushMetadataById({
|
||||
// id: productId.value,
|
||||
// });
|
||||
// message.success('配置应用成功');
|
||||
} catch {
|
||||
message.error('配置应用失败');
|
||||
}
|
||||
|
@ -128,7 +134,7 @@ onUnmounted(() => {
|
|||
:disabled="currentProduct.enabled === '0'"
|
||||
v-access:code="['device:product:edit']"
|
||||
>
|
||||
应用配置
|
||||
应用到设备
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -139,29 +145,15 @@ onUnmounted(() => {
|
|||
</div>
|
||||
|
||||
<!-- 标签页内容 -->
|
||||
<Tabs
|
||||
v-model:active-key="activeTab"
|
||||
class="detail-tabs"
|
||||
@change="handleTabChange"
|
||||
>
|
||||
<Tabs v-model:active-key="activeTab" class="detail-tabs" @change="handleTabChange">
|
||||
<TabPane key="BasicInfo" tab="配置信息">
|
||||
<BasicInfo
|
||||
:product-info="currentProduct"
|
||||
@refresh="loadProductInfo"
|
||||
/>
|
||||
<BasicInfo :product-info="currentProduct" @refresh="loadProductInfo" />
|
||||
</TabPane>
|
||||
<TabPane key="Metadata" tab="物模型">
|
||||
<Metadata
|
||||
:product-id="productId"
|
||||
:product-info="currentProduct"
|
||||
@refresh="loadProductInfo"
|
||||
/>
|
||||
<Metadata :product-id="productId" :product-info="currentProduct" @refresh="loadProductInfo" />
|
||||
</TabPane>
|
||||
<TabPane key="DeviceAccess" tab="设备接入">
|
||||
<DeviceAccess
|
||||
:product-info="currentProduct"
|
||||
@refresh="loadProductInfo"
|
||||
/>
|
||||
<DeviceAccess :product-info="currentProduct" @refresh="loadProductInfo" />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
|
|
@ -35,12 +35,10 @@ const [BasicForm, formApi] = useVbenForm({
|
|||
wrapperClass: 'grid-cols-2',
|
||||
});
|
||||
|
||||
const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(
|
||||
{
|
||||
initializedGetter: defaultFormValueGetter(formApi),
|
||||
currentGetter: defaultFormValueGetter(formApi),
|
||||
},
|
||||
);
|
||||
const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff({
|
||||
initializedGetter: defaultFormValueGetter(formApi),
|
||||
currentGetter: defaultFormValueGetter(formApi),
|
||||
});
|
||||
|
||||
const [BasicDrawer, drawerApi] = useVbenDrawer({
|
||||
// 在这里更改宽度
|
||||
|
@ -57,6 +55,7 @@ const [BasicDrawer, drawerApi] = useVbenDrawer({
|
|||
|
||||
const { id } = drawerApi.getData() as { id?: number | string };
|
||||
isUpdate.value = !!id;
|
||||
formApi.updateSchema([{ componentProps: { disabled: isUpdate.value }, fieldName: 'productKey' }]);
|
||||
|
||||
if (isUpdate.value && id) {
|
||||
const record = await productInfo(id);
|
||||
|
@ -65,6 +64,7 @@ const [BasicDrawer, drawerApi] = useVbenDrawer({
|
|||
await markInitialized();
|
||||
|
||||
drawerApi.drawerLoading(false);
|
||||
loadFormOptions();
|
||||
},
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in New Issue