平台端物模型导入导出
This commit is contained in:
parent
15cc3c7b14
commit
7d2fd92c32
@ -2489,7 +2489,7 @@ export const DeviceFirmwareInfoDtoPagedResultDtoSchema = {
|
||||
} as const;
|
||||
|
||||
export const DeviceLifeTypeEnumSchema = {
|
||||
enum: ['Unspecified', 'Production', 'FactoryTest', 'AgingSource', 'FactoryDelivery', 'Running', 'Maintenance', 'ReturnToFactory', 'Deactivated'],
|
||||
enum: ['Unspecified', 'Production', 'FactoryTest', 'AgingSource', 'FactoryDelivery', 'Running', 'Maintenance', 'ReturnToFactory', 'Deactivated', 'Banned'],
|
||||
type: 'string',
|
||||
description: `设备生命周期类型(业务运行态,与物模型上报 DataType、OneNET 连接生命周期无关)。
|
||||
软件侧可见起点为 JiShe.ServicePro.Enums.DeviceLifeTypeEnum.FactoryTest:组件完成且固件烧录通过后,上位机设置运行参数并接入平台。`
|
||||
@ -2650,7 +2650,17 @@ export const DeviceManagementInfoDtoSchema = {
|
||||
},
|
||||
platformPassword: {
|
||||
type: 'string',
|
||||
description: '物联网平台设备密码',
|
||||
description: '物联网平台设备密码(目标密钥:设备切到目标平台后应当使用的鉴权密钥)',
|
||||
nullable: true
|
||||
},
|
||||
deviceEffectiveSecKey: {
|
||||
type: 'string',
|
||||
description: '设备当前实际持有的密钥(生效密钥),指令报文的 AES 加密与签名用它',
|
||||
nullable: true
|
||||
},
|
||||
pendingSecKey: {
|
||||
type: 'string',
|
||||
description: '已下发、等待设备确认生效的目标密钥',
|
||||
nullable: true
|
||||
},
|
||||
ioTPlatformProductId: {
|
||||
@ -2801,6 +2811,14 @@ export const DeviceManagementInfoDtoSchema = {
|
||||
description: '最后主站地址',
|
||||
nullable: true
|
||||
},
|
||||
masterStationConfigs: {
|
||||
type: 'array',
|
||||
items: {
|
||||
'$ref': '#/components/schemas/DeviceMasterStationEndpoint'
|
||||
},
|
||||
description: '本次下发的主站配置集合(主用 + 备用),记录每个端点的地址与所属平台。',
|
||||
nullable: true
|
||||
},
|
||||
dataPushOverride: {
|
||||
type: 'boolean',
|
||||
description: '是否覆盖产品级推送配置:true 用本设备的推送配置,false 继承所属产品配置'
|
||||
@ -2951,6 +2969,26 @@ export const DeviceManagementInfoDtoPagedResultDtoHttpDataResultSchema = {
|
||||
description: 'HTTP 请求返回结果'
|
||||
} as const;
|
||||
|
||||
export const DeviceMasterStationEndpointSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
masterStationAddress: {
|
||||
type: 'string',
|
||||
description: '主站地址(host:port)',
|
||||
nullable: true
|
||||
},
|
||||
ioTPlatform: {
|
||||
'$ref': '#/components/schemas/IoTPlatformTypeEnum'
|
||||
}
|
||||
},
|
||||
additionalProperties: false,
|
||||
description: `设备主站端点:一个主站的地址 + 所属物联网平台。
|
||||
设备一次切主站会下发主用 + 备用两个端点,主备共用同一把接入密钥。
|
||||
服务端据此在切换确认后判断"主或备是否含 OneNET",进而决定是否要把设备当前密钥同步到 OneNET(一机一密)——
|
||||
否则设备自主回落到 OneNET 备用主站时,OneNET 侧仍是旧密钥、鉴权不过、连不上也报不了事件,彻底失联。
|
||||
以 JSON 序列化存 jsonb,供实体 / DTO / 缓存共用。`
|
||||
} as const;
|
||||
|
||||
export const DeviceMasterSwitchInputSchema = {
|
||||
required: ['backupMasterStation', 'id', 'ioTPlatformProductId', 'masterStation'],
|
||||
type: 'object',
|
||||
@ -4479,6 +4517,23 @@ export const EntityExtensionDtoSchema = {
|
||||
additionalProperties: false
|
||||
} as const;
|
||||
|
||||
export const ExportPlatformThingModelInputSchema = {
|
||||
required: ['ioTPlatformProductId'],
|
||||
type: 'object',
|
||||
properties: {
|
||||
ioTPlatformProductId: {
|
||||
minLength: 1,
|
||||
type: 'string',
|
||||
description: '平台产品ID'
|
||||
},
|
||||
platform: {
|
||||
'$ref': '#/components/schemas/IoTPlatformTypeEnum'
|
||||
}
|
||||
},
|
||||
additionalProperties: false,
|
||||
description: '导出平台端物模型入参(单个产品)'
|
||||
} as const;
|
||||
|
||||
export const ExtensionEnumDtoSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@ -6381,6 +6436,43 @@ export const IdentityUserUpdateDtoSchema = {
|
||||
additionalProperties: false
|
||||
} as const;
|
||||
|
||||
export const ImportPlatformThingModelResultDtoSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
ioTPlatformProductId: {
|
||||
type: 'string',
|
||||
description: '导入目标产品ID',
|
||||
nullable: true
|
||||
},
|
||||
sourceProductId: {
|
||||
type: 'string',
|
||||
description: '导入文件中记录的源产品ID',
|
||||
nullable: true
|
||||
},
|
||||
thingModelCount: {
|
||||
type: 'integer',
|
||||
description: '导入的物模型标识符条数',
|
||||
format: 'int32'
|
||||
},
|
||||
extensionCount: {
|
||||
type: 'integer',
|
||||
description: '导入的扩展子字段条数',
|
||||
format: 'int32'
|
||||
},
|
||||
commandCount: {
|
||||
type: 'integer',
|
||||
description: '导入的默认操作指令条数',
|
||||
format: 'int32'
|
||||
},
|
||||
success: {
|
||||
type: 'boolean',
|
||||
description: '是否导入成功(已覆盖数据库并刷新 Redis 缓存)'
|
||||
}
|
||||
},
|
||||
additionalProperties: false,
|
||||
description: '平台端物模型导入结果(单个产品)'
|
||||
} as const;
|
||||
|
||||
export const InterfaceMethodApiDescriptionModelSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -1410,7 +1410,7 @@ export type DeviceFirmwareInfoDtoPagedResultDto = {
|
||||
* 设备生命周期类型(业务运行态,与物模型上报 DataType、OneNET 连接生命周期无关)。
|
||||
* 软件侧可见起点为 JiShe.ServicePro.Enums.DeviceLifeTypeEnum.FactoryTest:组件完成且固件烧录通过后,上位机设置运行参数并接入平台。
|
||||
*/
|
||||
export type DeviceLifeTypeEnum = 'Unspecified' | 'Production' | 'FactoryTest' | 'AgingSource' | 'FactoryDelivery' | 'Running' | 'Maintenance' | 'ReturnToFactory' | 'Deactivated';
|
||||
export type DeviceLifeTypeEnum = 'Unspecified' | 'Production' | 'FactoryTest' | 'AgingSource' | 'FactoryDelivery' | 'Running' | 'Maintenance' | 'ReturnToFactory' | 'Deactivated' | 'Banned';
|
||||
|
||||
/**
|
||||
* 设备登录(EMQX HTTP 认证回调请求体)
|
||||
@ -1507,9 +1507,17 @@ export type DeviceManagementInfoDto = {
|
||||
*/
|
||||
ioTPlatformDeviceOpenInfo?: (string) | null;
|
||||
/**
|
||||
* 物联网平台设备密码
|
||||
* 物联网平台设备密码(目标密钥:设备切到目标平台后应当使用的鉴权密钥)
|
||||
*/
|
||||
platformPassword?: (string) | null;
|
||||
/**
|
||||
* 设备当前实际持有的密钥(生效密钥),指令报文的 AES 加密与签名用它
|
||||
*/
|
||||
deviceEffectiveSecKey?: (string) | null;
|
||||
/**
|
||||
* 已下发、等待设备确认生效的目标密钥
|
||||
*/
|
||||
pendingSecKey?: (string) | null;
|
||||
/**
|
||||
* 物联网平台中对应的产品Id
|
||||
*/
|
||||
@ -1610,6 +1618,10 @@ export type DeviceManagementInfoDto = {
|
||||
* 最后主站地址
|
||||
*/
|
||||
finalMainStation?: (string) | null;
|
||||
/**
|
||||
* 本次下发的主站配置集合(主用 + 备用),记录每个端点的地址与所属平台。
|
||||
*/
|
||||
masterStationConfigs?: Array<DeviceMasterStationEndpoint> | null;
|
||||
/**
|
||||
* 是否覆盖产品级推送配置:true 用本设备的推送配置,false 继承所属产品配置
|
||||
*/
|
||||
@ -1713,6 +1725,21 @@ export type DeviceManagementInfoDtoPagedResultDtoHttpDataResult = {
|
||||
total?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* 设备主站端点:一个主站的地址 + 所属物联网平台。
|
||||
* 设备一次切主站会下发主用 + 备用两个端点,主备共用同一把接入密钥。
|
||||
* 服务端据此在切换确认后判断"主或备是否含 OneNET",进而决定是否要把设备当前密钥同步到 OneNET(一机一密)——
|
||||
* 否则设备自主回落到 OneNET 备用主站时,OneNET 侧仍是旧密钥、鉴权不过、连不上也报不了事件,彻底失联。
|
||||
* 以 JSON 序列化存 jsonb,供实体 / DTO / 缓存共用。
|
||||
*/
|
||||
export type DeviceMasterStationEndpoint = {
|
||||
/**
|
||||
* 主站地址(host:port)
|
||||
*/
|
||||
masterStationAddress?: (string) | null;
|
||||
ioTPlatform?: IoTPlatformTypeEnum;
|
||||
};
|
||||
|
||||
/**
|
||||
* 设备切换主站入参
|
||||
*/
|
||||
@ -2685,6 +2712,17 @@ export type EntityExtensionDto = {
|
||||
} | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* 导出平台端物模型入参(单个产品)
|
||||
*/
|
||||
export type ExportPlatformThingModelInput = {
|
||||
/**
|
||||
* 平台产品ID
|
||||
*/
|
||||
ioTPlatformProductId: string;
|
||||
platform?: IoTPlatformTypeEnum;
|
||||
};
|
||||
|
||||
export type ExtensionEnumDto = {
|
||||
fields?: Array<ExtensionEnumFieldDto> | null;
|
||||
localizationResource?: (string) | null;
|
||||
@ -3381,6 +3419,36 @@ export type IdInput = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* 平台端物模型导入结果(单个产品)
|
||||
*/
|
||||
export type ImportPlatformThingModelResultDto = {
|
||||
/**
|
||||
* 导入目标产品ID
|
||||
*/
|
||||
ioTPlatformProductId?: (string) | null;
|
||||
/**
|
||||
* 导入文件中记录的源产品ID
|
||||
*/
|
||||
sourceProductId?: (string) | null;
|
||||
/**
|
||||
* 导入的物模型标识符条数
|
||||
*/
|
||||
thingModelCount?: number;
|
||||
/**
|
||||
* 导入的扩展子字段条数
|
||||
*/
|
||||
extensionCount?: number;
|
||||
/**
|
||||
* 导入的默认操作指令条数
|
||||
*/
|
||||
commandCount?: number;
|
||||
/**
|
||||
* 是否导入成功(已覆盖数据库并刷新 Redis 缓存)
|
||||
*/
|
||||
success?: boolean;
|
||||
};
|
||||
|
||||
export type InterfaceMethodApiDescriptionModel = {
|
||||
name?: (string) | null;
|
||||
parametersOnMethod?: Array<MethodParameterApiDescriptionModel> | null;
|
||||
@ -8187,6 +8255,42 @@ export type PostIoTplatformThingModelInfoCreateIoTplatformThingModelCommandRespo
|
||||
|
||||
export type PostIoTplatformThingModelInfoCreateIoTplatformThingModelCommandError = unknown;
|
||||
|
||||
export type PostIoTplatformThingModelInfoExportPlatformThingModelData = {
|
||||
query?: {
|
||||
/**
|
||||
* 导出参数(产品ID 必填,平台类型默认 OneNET)
|
||||
*/
|
||||
input?: ExportPlatformThingModelInput;
|
||||
};
|
||||
};
|
||||
|
||||
export type PostIoTplatformThingModelInfoExportPlatformThingModelResponse = ((Blob | File));
|
||||
|
||||
export type PostIoTplatformThingModelInfoExportPlatformThingModelError = unknown;
|
||||
|
||||
export type PostIoTplatformThingModelInfoImportPlatformThingModelData = {
|
||||
body?: {
|
||||
/**
|
||||
* 导出的 JSON 文件
|
||||
*/
|
||||
file?: (Blob | File);
|
||||
};
|
||||
query?: {
|
||||
/**
|
||||
* 导入目标产品ID
|
||||
*/
|
||||
ioTPlatformProductId?: string;
|
||||
/**
|
||||
* 平台类型,默认 OneNET
|
||||
*/
|
||||
platform?: IoTPlatformTypeEnum;
|
||||
};
|
||||
};
|
||||
|
||||
export type PostIoTplatformThingModelInfoImportPlatformThingModelResponse = (ImportPlatformThingModelResultDto);
|
||||
|
||||
export type PostIoTplatformThingModelInfoImportPlatformThingModelError = unknown;
|
||||
|
||||
export type PostLanguagesAllResponse = (Array<PageLanguageOutput>);
|
||||
|
||||
export type PostLanguagesAllError = (RemoteServiceErrorResponse);
|
||||
|
||||
@ -19,7 +19,9 @@ import {
|
||||
postIoTplatformThingModelInfoCopyStandardThingModel,
|
||||
postIoTplatformThingModelInfoCreateAsync,
|
||||
postIoTplatformThingModelInfoDeleteAsync,
|
||||
postIoTplatformThingModelInfoExportPlatformThingModel,
|
||||
postIoTplatformThingModelInfoGetIoTplatformThingModelService,
|
||||
postIoTplatformThingModelInfoImportPlatformThingModel,
|
||||
postIoTplatformThingModelInfoPageAsync,
|
||||
postIoTplatformThingModelInfoUpdateAsync,
|
||||
} from '#/api-client';
|
||||
@ -229,6 +231,11 @@ const editRow: Record<string, any> = ref({});
|
||||
const hasData = ref(true);
|
||||
// 模型刷新按钮 loading 状态
|
||||
const thingModelRefreshLoading = ref(false);
|
||||
// 导出/导入按钮 loading 状态
|
||||
const exportLoading = ref(false);
|
||||
const importLoading = ref(false);
|
||||
// 导入文件选择框引用(隐藏 input)
|
||||
const importFileInputRef = ref<HTMLInputElement | null>(null);
|
||||
|
||||
const [ThingModelModal, thingModelModalApi] = useVbenModal({
|
||||
draggable: true,
|
||||
@ -1063,6 +1070,121 @@ async function onThingModelRefresh() {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前搜索表单中的平台/产品(导出、导入共用)
|
||||
async function getCurrentPlatformAndProduct() {
|
||||
const formValues = gridApi?.formApi ? await gridApi.formApi.getValues() : {};
|
||||
const currentProductId = String(
|
||||
formValues.ioTPlatformProductId || productId.value || '',
|
||||
);
|
||||
const platformNum =
|
||||
Number.parseInt(
|
||||
String(formValues.ioTPlatform || ioTPlatform.value || '2'),
|
||||
10,
|
||||
) || 2;
|
||||
return { currentProductId, platformNum };
|
||||
}
|
||||
|
||||
// 导出当前产品的平台端物模型 JSON
|
||||
async function onExportThingModel() {
|
||||
const { currentProductId, platformNum } =
|
||||
await getCurrentPlatformAndProduct();
|
||||
if (!currentProductId) {
|
||||
Message.error('请先选择产品再导出');
|
||||
return;
|
||||
}
|
||||
if (!hasData.value) {
|
||||
Message.error('当前产品暂无可导出的平台端物模型');
|
||||
return;
|
||||
}
|
||||
|
||||
exportLoading.value = true;
|
||||
try {
|
||||
const { data } = await postIoTplatformThingModelInfoExportPlatformThingModel(
|
||||
{
|
||||
// 后端复杂入参按 body 绑定(生成的 query.input 为 nswag 产物)
|
||||
body: {
|
||||
ioTPlatformProductId: currentProductId,
|
||||
platform: platformNum,
|
||||
},
|
||||
responseType: 'blob',
|
||||
},
|
||||
);
|
||||
const url = window.URL.createObjectURL(new Blob([data as Blob]));
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
const now = new Date();
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
const stamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
||||
link.setAttribute(
|
||||
'download',
|
||||
`平台端物模型_${currentProductId}_${stamp}.json`,
|
||||
);
|
||||
document.body.append(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
Message.success('导出成功');
|
||||
} catch (error) {
|
||||
console.error('导出平台端物模型失败:', error);
|
||||
Message.error('导出失败,请确认该产品已配置平台端物模型');
|
||||
} finally {
|
||||
exportLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 触发隐藏文件选择框
|
||||
function triggerImportThingModel() {
|
||||
if (!productId.value) {
|
||||
Message.error('请先选择导入目标产品');
|
||||
return;
|
||||
}
|
||||
importFileInputRef.value?.click();
|
||||
}
|
||||
|
||||
// 选择文件后执行导入
|
||||
async function onImportThingModelFileChange(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
const file = input.files?.[0];
|
||||
// 重置 input 值,保证同一文件可再次选择触发 change
|
||||
input.value = '';
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { currentProductId, platformNum } =
|
||||
await getCurrentPlatformAndProduct();
|
||||
if (!currentProductId) {
|
||||
Message.error('请先选择导入目标产品');
|
||||
return;
|
||||
}
|
||||
|
||||
importLoading.value = true;
|
||||
try {
|
||||
const { data } = await postIoTplatformThingModelInfoImportPlatformThingModel(
|
||||
{
|
||||
query: {
|
||||
ioTPlatformProductId: currentProductId,
|
||||
platform: platformNum as any,
|
||||
},
|
||||
body: { file },
|
||||
},
|
||||
);
|
||||
if (data?.success) {
|
||||
Message.success(
|
||||
`导入成功:物模型 ${data.thingModelCount ?? 0} 条、扩展 ${data.extensionCount ?? 0} 条、指令 ${data.commandCount ?? 0} 条`,
|
||||
);
|
||||
await gridApi.reload();
|
||||
} else {
|
||||
Message.error('导入失败');
|
||||
}
|
||||
} catch (error) {
|
||||
// 具体失败原因(如目标产品不存在、文件解析失败)由全局错误拦截器统一提示
|
||||
console.error('导入平台端物模型失败:', error);
|
||||
} finally {
|
||||
importLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 打开复制已有模型模态框
|
||||
const openCopyAnotherThingModelModal = async () => {
|
||||
console.log('打开复制模态框,当前参数:', {
|
||||
@ -1228,6 +1350,24 @@ async function onDel(record: any) {
|
||||
disabled: !productId,
|
||||
loading: thingModelRefreshLoading,
|
||||
},
|
||||
{
|
||||
label: '导出JSON',
|
||||
type: 'default',
|
||||
icon: 'ant-design:download-outlined',
|
||||
onClick: onExportThingModel,
|
||||
auth: ['AbpIdentity.Users.Create'],
|
||||
disabled: !productId,
|
||||
loading: exportLoading,
|
||||
},
|
||||
{
|
||||
label: '导入JSON',
|
||||
type: 'default',
|
||||
icon: 'ant-design:upload-outlined',
|
||||
onClick: triggerImportThingModel,
|
||||
auth: ['AbpIdentity.Users.Create'],
|
||||
disabled: !productId,
|
||||
loading: importLoading,
|
||||
},
|
||||
]" />
|
||||
</template>
|
||||
|
||||
@ -1300,6 +1440,15 @@ async function onDel(record: any) {
|
||||
]" />
|
||||
</template>
|
||||
</Grid>
|
||||
<!-- 隐藏的导入文件选择框 -->
|
||||
<input
|
||||
ref="importFileInputRef"
|
||||
type="file"
|
||||
accept=".json,application/json"
|
||||
style="display: none"
|
||||
@change="onImportThingModelFileChange"
|
||||
/>
|
||||
|
||||
<ThingModelModal :title="editRow.id ? $t('common.edit') : $t('common.add')" class="w-[800px]">
|
||||
<component :is="editRow.id ? EditForm : AddForm" />
|
||||
</ThingModelModal>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user