diff --git a/apps/web-antd/src/api-client/schemas.gen.ts b/apps/web-antd/src/api-client/schemas.gen.ts index a9144ee..823391a 100644 --- a/apps/web-antd/src/api-client/schemas.gen.ts +++ b/apps/web-antd/src/api-client/schemas.gen.ts @@ -5352,6 +5352,28 @@ export const GetPermissionInputSchema = { additionalProperties: false } as const; +export const GetPlatformThingModelServiceInputSchema = { + required: ['id'], + type: 'object', + properties: { + id: { + minLength: 1, + type: 'string' + }, + standardFieldName: { + type: 'string', + description: '管理后台产品标准的物模型属性或者事件名称', + nullable: true + }, + isGetOperateService: { + type: 'boolean', + description: '是否只获取操作服务' + } + }, + additionalProperties: false, + description: '获取平台物模型服务列表入参' +} as const; + export const GetQRCodeOutputSchema = { type: 'object', properties: { diff --git a/apps/web-antd/src/api-client/types.gen.ts b/apps/web-antd/src/api-client/types.gen.ts index ce40802..2db0025 100644 --- a/apps/web-antd/src/api-client/types.gen.ts +++ b/apps/web-antd/src/api-client/types.gen.ts @@ -2893,6 +2893,21 @@ export type GetPermissionInput = { providerKey?: (string) | null; }; +/** + * 获取平台物模型服务列表入参 + */ +export type GetPlatformThingModelServiceInput = { + id: string; + /** + * 管理后台产品标准的物模型属性或者事件名称 + */ + standardFieldName?: (string) | null; + /** + * 是否只获取操作服务 + */ + isGetOperateService?: boolean; +}; + export type GetQRCodeOutput = { /** * base64 二维码 @@ -7770,7 +7785,7 @@ export type PostIoTplatformThingModelInfoUpdateOperableIdentifierError = unknown export type PostIoTplatformThingModelInfoGetIoTplatformThingModelServiceData = { query?: { - input?: StringIdInput; + input?: GetPlatformThingModelServiceInput; }; }; diff --git a/apps/web-antd/src/views/devicemanagement/deviceinfo/index.vue b/apps/web-antd/src/views/devicemanagement/deviceinfo/index.vue index 30a4559..df7a1ac 100644 --- a/apps/web-antd/src/views/devicemanagement/deviceinfo/index.vue +++ b/apps/web-antd/src/views/devicemanagement/deviceinfo/index.vue @@ -816,7 +816,10 @@ function isOperateBreakerServiceItem(item: { /** 拉合闸类型参数:与物模型 InputData 标识符 / 展示名对应 */ function isBreakerOperateTypeField(f: ServiceParamField): boolean { const n = f.name.trim().toLowerCase(); - if (f.label.includes('拉合闸类型')) { + if (f.label.includes('拉合闸类型') || f.label.includes('操作类型')) { + return true; + } + if (n === 'operatetype' || n === 'operate_type') { return true; } if (n === 'operatecommandtype') { @@ -858,11 +861,6 @@ const valveCommandTypeOptions = ref>( ); const valveCommandTypeLoading = ref(false); -const breakerOperateTypeOptions = ref>( - [], -); -const breakerOperateTypeLoading = ref(false); - function normalizeServiceThirdValue(raw: unknown): ServiceParamField[] { if (raw === null || raw === undefined || raw === '') { return []; @@ -993,6 +991,10 @@ const serviceCallSelectOptions = computed(() => })), ); +const breakerOperateTypeOptions = computed(() => + extractBreakerOperateTypeOptionsFromSecondValue(currentServiceItem.value), +); + function shouldUseValveCommandTypeSelectForField(f: ServiceParamField): boolean { if (!isValveControlServiceItem(currentServiceItem.value)) { return false; @@ -1009,6 +1011,96 @@ function shouldUseBreakerOperateTypeSelectForField( return isBreakerOperateTypeField(f); } +function parseJsonMaybe(raw: unknown): unknown { + if (typeof raw !== 'string') { + return raw; + } + const s = raw.trim(); + if (!s) { + return null; + } + try { + return JSON.parse(s) as unknown; + } catch { + return null; + } +} + +function toSecondValueSelectOptions( + list: unknown[], +): Array<{ label: string; value: string }> { + return list + .map((x: any) => { + if (!x || typeof x !== 'object') { + return null; + } + const value = String( + x.key ?? x.id ?? x.value ?? x.Identifier ?? x.identifier ?? '', + ).trim(); + const label = String( + x.label ?? x.name ?? x.text ?? x.value ?? x.Name ?? x.name ?? value, + ).trim(); + if (!value) { + return null; + } + return { label: label || value, value }; + }) + .filter(Boolean) as Array<{ label: string; value: string }>; +} + +function extractBreakerOperateTypeOptionsFromSecondValue(item: { + secondValue?: null | string; +} | null): Array<{ label: string; value: string }> { + if (!item) { + return []; + } + const parsed = parseJsonMaybe(item.secondValue); + if (!parsed) { + return []; + } + + if (Array.isArray(parsed)) { + return toSecondValueSelectOptions(parsed); + } + + if (typeof parsed === 'object') { + const obj = parsed as Record; + const candidateLists = [ + obj.operateType, + obj.OperateType, + obj.operateTypeList, + obj.operateTypes, + obj.OperateTypeList, + obj.OperateTypes, + obj.options, + obj.list, + obj.items, + obj.data, + ]; + for (const c of candidateLists) { + if (Array.isArray(c)) { + const options = toSecondValueSelectOptions(c); + if (options.length > 0) { + return options; + } + } + // secondValue 也可能是键值映射:{ "1":"拉闸", "2":"合闸" } + if (c && typeof c === 'object' && !Array.isArray(c)) { + const mapped = Object.entries(c as Record).map( + ([k, v]) => ({ + label: String(v ?? k), + value: String(k), + }), + ); + if (mapped.length > 0) { + return mapped; + } + } + } + } + return []; +} + function mapSelectListToOptions(list: any[]): Array<{ label: string; value: string }> { return list .map((x: any) => ({ @@ -1035,7 +1127,6 @@ watch( () => currentServiceItem.value, async (item) => { valveCommandTypeOptions.value = []; - breakerOperateTypeOptions.value = []; if (!item) { return; } @@ -1056,21 +1147,6 @@ watch( } } - if (isOperateBreakerServiceItem(item)) { - breakerOperateTypeLoading.value = true; - try { - const { data } = await getCommonGetSelectList({ - query: { typeName: DEVICE_THING_MODE_COMMAND_TYPE_ENUM }, - } as any); - breakerOperateTypeOptions.value = mapSelectListToOptions( - unwrapSelectListResponse(data), - ); - } catch { - breakerOperateTypeOptions.value = []; - } finally { - breakerOperateTypeLoading.value = false; - } - } }, ); @@ -1107,8 +1183,6 @@ const [ServiceCallModal, serviceCallModalApi] = useVbenModal({ serviceCallFormValues.value = {}; valveCommandTypeOptions.value = []; valveCommandTypeLoading.value = false; - breakerOperateTypeOptions.value = []; - breakerOperateTypeLoading.value = false; return true; }, onOpenChange: async (isOpen: boolean) => { @@ -1126,6 +1200,22 @@ const [ServiceCallModal, serviceCallModalApi] = useVbenModal({ const serviceCallModalState = serviceCallModalApi.useStore(); +/** 服务入参:部分字段需为数值(如 Quantity),避免以字符串提交 */ +function coerceServiceCallParamValue( + fieldName: string, + raw: string, +): { ok: true; value: unknown } | { ok: false; message: string } { + const key = fieldName.trim(); + if (key.toLowerCase() === 'quantity') { + const n = Number(raw); + if (Number.isNaN(n)) { + return { ok: false, message: '数量(Quantity)需填写有效数字' }; + } + return { ok: true, value: n }; + } + return { ok: true, value: raw }; +} + async function submitDeviceServiceCall() { const row = serviceCallRow.value; if (!row?.id) { @@ -1149,7 +1239,12 @@ async function submitDeviceServiceCall() { for (const f of serviceParamFields.value) { const v = (serviceCallFormValues.value[f.name] ?? '').trim(); if (v !== '') { - serviceParams[f.name] = v; + const coerced = coerceServiceCallParamValue(f.name, v); + if (!coerced.ok) { + Message.warning(coerced.message); + return; + } + serviceParams[f.name] = coerced.value; } } try { @@ -2861,7 +2956,6 @@ const toolbarActions = computed(() => [ v-else-if="shouldUseBreakerOperateTypeSelectForField(f)" v-model:value="serviceCallFormValues[f.name]" :options="breakerOperateTypeOptions" - :loading="breakerOperateTypeLoading" allow-clear class="min-w-0 flex-1" size="small" diff --git a/apps/web-antd/src/views/thingmodelinfo/ioTPlatformThingModelInfo/index.vue b/apps/web-antd/src/views/thingmodelinfo/ioTPlatformThingModelInfo/index.vue index ce51e73..dac552a 100644 --- a/apps/web-antd/src/views/thingmodelinfo/ioTPlatformThingModelInfo/index.vue +++ b/apps/web-antd/src/views/thingmodelinfo/ioTPlatformThingModelInfo/index.vue @@ -7,17 +7,19 @@ import { useRoute } from 'vue-router'; import { Page, useVbenModal } from '@vben/common-ui'; -import { message as Message, Tag } from 'ant-design-vue'; +import { Button, Input, Select, message as Message, Tag } from 'ant-design-vue'; import { useVbenForm } from '#/adapter/form'; import { useVbenVxeGrid } from '#/adapter/vxe-table'; import { getCommonGetSelectList, postAggregationIoTplatformUpdateIoTplatformProductThingModelInfoAsync, + postIoTplatformThingModelInfoCreateIoTplatformThingModelCommand, postIoTplatformThingModelInfoCopyAnotherThingModelAsync, postIoTplatformThingModelInfoCopyStandardThingModel, postIoTplatformThingModelInfoCreateAsync, postIoTplatformThingModelInfoDeleteAsync, + postIoTplatformThingModelInfoGetIoTplatformThingModelService, postIoTplatformThingModelInfoPageAsync, postIoTplatformThingModelInfoUpdateAsync, } from '#/api-client'; @@ -289,6 +291,14 @@ const [ThingModelModal, thingModelModalApi] = useVbenModal({ await formApi.setValues({ ...(isEdit ? editRow.value : {}), + ...(isEdit && { + standardFieldFieldExtension: formatFieldExtensionForForm( + editRow.value.standardFieldFieldExtension, + ), + ioTPlatformRawFieldExtension: formatFieldExtensionForForm( + editRow.value.ioTPlatformRawFieldExtension, + ), + }), _ioTPlatform: platformValue, _ioTPlatformProductId: productIdValue, // 确保 identifierType 转换为字符串格式,以便与下拉框的 key 匹配 @@ -355,6 +365,105 @@ const [CopyStandardModal, copyStandardModalApi] = useVbenModal({ }, }); +type OperateServiceItem = { + key?: null | string; + value?: null | string; + secondValue?: null | string; + thirdValue?: unknown; +}; + +const commandEditRow = ref>({}); +const operateServiceLoading = ref(false); +const createCommandLoading = ref(false); +const operateServiceOptions = ref([]); +const operateTypeOptions = ref>([]); +const selectedOperateType = ref(); +const operateIssueCommandText = ref(''); + +function normalizeOperateTypeOptions( + raw: unknown, +): Array<{ label: string; value: number }> { + if (raw == null || raw === '') { + return []; + } + + let data: unknown = raw; + if (typeof data === 'string') { + const s = data.trim(); + if (!s) { + return []; + } + try { + data = JSON.parse(s) as unknown; + } catch { + return []; + } + } + + if (Array.isArray(data)) { + return data + .map((item: any) => { + const valueRaw = item?.key ?? item?.value ?? item?.id ?? item?.operateType; + const labelRaw = + item?.value ?? + item?.label ?? + item?.name ?? + item?.displayName ?? + item?.key; + const value = Number.parseInt(String(valueRaw ?? ''), 10); + if (Number.isNaN(value)) { + return null; + } + return { + value, + label: String(labelRaw ?? value), + }; + }) + .filter(Boolean) as Array<{ label: string; value: number }>; + } + + if (typeof data === 'object') { + return Object.entries(data as Record) + .map(([k, v]) => { + const value = Number.parseInt(String(k), 10); + if (Number.isNaN(value)) { + return null; + } + return { + value, + label: String(v ?? k), + }; + }) + .filter(Boolean) as Array<{ label: string; value: number }>; + } + + return []; +} + +const [OperateCommandModal, operateCommandModalApi] = useVbenModal({ + draggable: true, + footer: true, + showCancelButton: true, + showConfirmButton: true, + confirmText: '添加', + onConfirm: submitOperateCommand, + onBeforeClose: () => { + commandEditRow.value = {}; + operateServiceOptions.value = []; + operateTypeOptions.value = []; + selectedOperateType.value = undefined; + operateIssueCommandText.value = ''; + createCommandLoading.value = false; + return true; + }, + onOpenChange: async (isOpen: boolean) => { + if (!isOpen) { + return; + } + await fetchOperateServiceOptions(); + }, +}); + // 创建新增表单 schema(传入获取平台和产品ID的函数,作为后备方案) // 注意:实际使用时,值会从表单的 _ioTPlatform 和 _ioTPlatformProductId 字段获取 const addThingModelFormSchema = getAddThingModelFormSchema( @@ -540,6 +649,20 @@ function normalizeFieldExtensionValue(fieldValue: unknown) { } } +function formatFieldExtensionForForm(fieldValue: unknown) { + if (fieldValue == null || fieldValue === '') { + return ''; + } + if (typeof fieldValue === 'string') { + return fieldValue; + } + try { + return JSON.stringify(fieldValue, null, 2); + } catch { + return String(fieldValue); + } +} + function parseJsonFieldValue( fieldValue: unknown, fieldLabel: string, @@ -685,6 +808,12 @@ async function onEdit(record: any) { setTimeout(async () => { await editFormApi.setValues({ ...record, + standardFieldFieldExtension: formatFieldExtensionForForm( + record.standardFieldFieldExtension, + ), + ioTPlatformRawFieldExtension: formatFieldExtensionForForm( + record.ioTPlatformRawFieldExtension, + ), _ioTPlatform: platformValue, _ioTPlatformProductId: productIdValue, // 确保 identifierType 转换为字符串格式,以便与下拉框的 key 匹配 @@ -755,6 +884,106 @@ const openAddModal = async () => { }, 100); }; +async function fetchOperateServiceOptions() { + if (!commandEditRow.value?.ioTPlatformProductId) { + Message.warning('当前物模型数据缺少产品ID,无法加载可操作指令'); + return; + } + if (!commandEditRow.value?.standardFieldName) { + Message.warning('当前物模型数据缺少标准物模型标识符,无法加载可操作指令'); + return; + } + + operateServiceLoading.value = true; + try { + const { data } = await postIoTplatformThingModelInfoGetIoTplatformThingModelService( + { + body: { + id: String(commandEditRow.value.ioTPlatformProductId), + standardFieldName: String(commandEditRow.value.standardFieldName), + isGetOperateService: true, + }, + }, + ); + operateServiceOptions.value = Array.isArray(data) + ? (data as OperateServiceItem[]) + : []; + const firstService = operateServiceOptions.value[0]; + operateTypeOptions.value = normalizeOperateTypeOptions( + firstService?.secondValue, + ); + selectedOperateType.value = + operateTypeOptions.value.length > 0 + ? operateTypeOptions.value[0]?.value + : undefined; + if (operateServiceOptions.value.length === 0) { + Message.warning('当前属性标识符暂无可操作指令'); + } + } catch (error) { + console.error('获取可操作指令列表失败:', error); + Message.error('获取可操作指令列表失败'); + operateServiceOptions.value = []; + operateTypeOptions.value = []; + selectedOperateType.value = undefined; + } finally { + operateServiceLoading.value = false; + } +} + +function openOperateCommandModal(record: Record) { + if (!record?.id) { + Message.warning('当前物模型数据缺少ID,无法添加默认操作指令'); + return; + } + const filedType = String(record?.filedType ?? ''); + if (!filedType.includes('Service')) { + Message.warning('仅服务类型物模型支持添加默认操作指令'); + return; + } + commandEditRow.value = record; + operateCommandModalApi.open(); +} + +async function submitOperateCommand() { + if (!commandEditRow.value?.id) { + return; + } + if (selectedOperateType.value == null) { + Message.warning('请选择操作类型'); + return; + } + + let issueCommand = operateIssueCommandText.value.trim(); + if (!issueCommand) { + Message.warning('请填写指令内容'); + return; + } + + createCommandLoading.value = true; + try { + const resp = + await postIoTplatformThingModelInfoCreateIoTplatformThingModelCommand({ + body: { + thingModelDataId: String(commandEditRow.value.id), + operateType: selectedOperateType.value, + issueCommand, + }, + }); + if (resp.data) { + Message.success('默认操作指令添加成功'); + operateCommandModalApi.close(); + await gridApi.reload(); + } else { + Message.error('默认操作指令添加失败'); + } + } catch (error) { + console.error('默认操作指令添加失败:', error); + Message.error('默认操作指令添加失败'); + } finally { + createCommandLoading.value = false; + } +} + // 模型刷新 async function onThingModelRefresh() { const formValues = gridApi?.formApi ? await gridApi.formApi.getValues() : {}; @@ -1023,6 +1252,14 @@ async function onDel(record: any) { confirm: onDel.bind(null, row), }, }, + { + label: '指令', + type: 'link', + size: 'small', + auth: ['AbpIdentity.Users.Update'], + ifShow: String(row.filedType ?? '').includes('Service'), + onClick: openOperateCommandModal.bind(null, row), + }, ]" /> @@ -1039,5 +1276,50 @@ async function onDel(record: any) { + + +
+ 正在加载可操作指令列表... +
+
+
+ 属性标识符:{{ commandEditRow.ioTPlatformRawFieldName || '-' }} +
+
+ 操作类型 +