平台的物模型操作完善

This commit is contained in:
ChenYi 2026-04-16 17:29:52 +08:00
parent 8f4cd47a4f
commit 095b521259
5 changed files with 454 additions and 28 deletions

View File

@ -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: {

View File

@ -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;
};
};

View File

@ -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<Array<{ label: string; value: string }>>(
);
const valveCommandTypeLoading = ref(false);
const breakerOperateTypeOptions = ref<Array<{ label: string; value: string }>>(
[],
);
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<string, unknown>;
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<string, unknown>).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"

View File

@ -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<Record<string, any>>({});
const operateServiceLoading = ref(false);
const createCommandLoading = ref(false);
const operateServiceOptions = ref<OperateServiceItem[]>([]);
const operateTypeOptions = ref<Array<{ label: string; value: number }>>([]);
const selectedOperateType = ref<number>();
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<string, unknown>)
.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();
},
});
// schemaID
// 使 _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<string, any>) {
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),
},
]" />
</template>
</Grid>
@ -1039,5 +1276,50 @@ async function onDel(record: any) {
<CopyStandardModal title="复制标准模型" class="w-[600px]">
<CopyStandardForm />
</CopyStandardModal>
<OperateCommandModal title="默认操作指令添加" class="w-[640px]">
<div v-if="operateServiceLoading" class="py-8 text-center text-gray-500">
正在加载可操作指令列表...
</div>
<div v-else class="space-y-3">
<div class="text-xs text-gray-500">
属性标识符{{ commandEditRow.ioTPlatformRawFieldName || '-' }}
</div>
<div class="flex items-center gap-2">
<span class="w-24 flex-shrink-0 text-right text-sm text-gray-600">操作类型</span>
<Select
v-model:value="selectedOperateType"
:options="operateTypeOptions"
allow-clear
class="min-w-0 flex-1"
placeholder="请选择操作类型"
size="small"
/>
</div>
<div class="space-y-1">
<div class="text-sm font-medium text-gray-700">指令内容</div>
<Input.TextArea
v-model:value="operateIssueCommandText"
:rows="4"
placeholder="请输入默认操作指令内容"
/>
</div>
</div>
<template #footer>
<div class="flex w-full items-center justify-end gap-2">
<Button @click="operateCommandModalApi.close()">
{{ $t('common.cancel') }}
</Button>
<Button
type="primary"
:loading="createCommandLoading"
:disabled="operateServiceLoading"
@click="submitOperateCommand"
>
添加
</Button>
</div>
</template>
</OperateCommandModal>
</Page>
</template>

View File

@ -828,9 +828,13 @@ export const getEditThingModelFormSchema = (
disabled: true, // 编辑时禁用
onResolve: (item: any | null) => {
formValues.standardFieldDisplayName = item?.displayText ?? '';
formValues.standardFieldName = item?.code ?? '';
formValues.standardFieldValueType = (item?.extendedAttribute ?? '')
.toString()
.toUpperCase();
formValues.standardFieldFieldExtension = formatFieldExtensionFormValue(
item?.extendedAttributeValue,
);
},
placeholder:
$t('common.pleaseInput') +
@ -849,6 +853,15 @@ export const getEditThingModelFormSchema = (
$t('abp.thingModelInfos.StandardFieldName'),
},
},
{
component: 'Textarea',
fieldName: 'standardFieldFieldExtension',
label: $t('abp.thingModelInfos.StandardFieldFieldExtension'),
componentProps: {
rows: 4,
placeholder: '请选择标准物模型后自动回填扩展参数JSON格式',
},
},
{
component: 'ApiSelect',
fieldName: 'ioTPlatformRawFieldName',