Merge pull request !234 from xingyu/dev
This commit is contained in:
xingyu
2025-10-20 02:54:38 +00:00
committed by Gitee
156 changed files with 24069 additions and 1542 deletions

View File

@@ -2,7 +2,7 @@ import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request'; import { requestClient } from '#/api/request';
namespace ErpFinanceReceiptApi { export namespace ErpFinanceReceiptApi {
/** 收款单项 */ /** 收款单项 */
export interface FinanceReceiptItem { export interface FinanceReceiptItem {
id?: number; id?: number;

View File

@@ -101,6 +101,7 @@ export interface Action {
identifier?: string; identifier?: string;
value?: any; value?: any;
alertConfigId?: number; alertConfigId?: number;
params?: string;
} }
/** 查询场景联动规则分页 */ /** 查询场景联动规则分页 */

View File

@@ -175,17 +175,27 @@ export function deleteThingModelList(ids: number[]) {
}); });
} }
/** 导入物模型 TSL */ /** 获取物模型 TSL */
export function getThingModelTSL(productId: number) {
return requestClient.get<ThingModelApi.ThingModel[]>(
'/iot/thing-model/get-tsl',
{ params: { productId } },
);
}
/** 导入物模型 TSL
export function importThingModelTSL(productId: number, tslData: any) { export function importThingModelTSL(productId: number, tslData: any) {
return requestClient.post('/iot/thing-model/import-tsl', { return requestClient.post('/iot/thing-model/import-tsl', {
productId, productId,
tslData, tslData,
}); });
} }
*/
/** 导出物模型 TSL */ /** 导出物模型 TSL
export function exportThingModelTSL(productId: number) { export function exportThingModelTSL(productId: number) {
return requestClient.get<any>('/iot/thing-model/export-tsl', { return requestClient.get<any>('/iot/thing-model/export-tsl', {
params: { productId }, params: { productId },
}); });
} }
*/

View File

@@ -740,7 +740,10 @@
"name": "FailedJobRetryTimeCycle", "name": "FailedJobRetryTimeCycle",
"superClass": ["Element"], "superClass": ["Element"],
"meta": { "meta": {
"allowedIn": ["activiti:AsyncCapable", "bpmn:MultiInstanceLoopCharacteristics"] "allowedIn": [
"activiti:AsyncCapable",
"bpmn:MultiInstanceLoopCharacteristics"
]
}, },
"properties": [ "properties": [
{ {

View File

@@ -727,7 +727,10 @@
"name": "FailedJobRetryTimeCycle", "name": "FailedJobRetryTimeCycle",
"superClass": ["Element"], "superClass": ["Element"],
"meta": { "meta": {
"allowedIn": ["camunda:AsyncCapable", "bpmn:MultiInstanceLoopCharacteristics"] "allowedIn": [
"camunda:AsyncCapable",
"bpmn:MultiInstanceLoopCharacteristics"
]
}, },
"properties": [ "properties": [
{ {

View File

@@ -910,7 +910,10 @@
"name": "FailedJobRetryTimeCycle", "name": "FailedJobRetryTimeCycle",
"superClass": ["Element"], "superClass": ["Element"],
"meta": { "meta": {
"allowedIn": ["flowable:AsyncCapable", "bpmn:MultiInstanceLoopCharacteristics"] "allowedIn": [
"flowable:AsyncCapable",
"bpmn:MultiInstanceLoopCharacteristics"
]
}, },
"properties": [ "properties": [
{ {

View File

@@ -18,7 +18,8 @@ const routes: RouteRecordRaw[] = [
title: '产品详情', title: '产品详情',
activePath: '/iot/device/product', activePath: '/iot/device/product',
}, },
component: () => import('#/views/iot/product/product/modules/detail/index.vue'), component: () =>
import('#/views/iot/product/product/modules/detail/index.vue'),
}, },
{ {
path: 'device/detail/:id', path: 'device/detail/:id',
@@ -27,11 +28,11 @@ const routes: RouteRecordRaw[] = [
title: '设备详情', title: '设备详情',
activePath: '/iot/device/device', activePath: '/iot/device/device',
}, },
component: () => import('#/views/iot/device/device/modules/detail/index.vue'), component: () =>
import('#/views/iot/device/device/modules/detail/index.vue'),
}, },
], ],
}, },
]; ];
export default routes; export default routes;

View File

@@ -109,7 +109,7 @@ export function useGridFormSchemaMessage(): VbenFormSchema[] {
label: '用户编号', label: '用户编号',
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
api: getSimpleUserList, api: () => getSimpleUserList(),
labelField: 'nickname', labelField: 'nickname',
valueField: 'id', valueField: 'id',
}, },

View File

@@ -15,7 +15,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
label: '用户编号', label: '用户编号',
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
api: getSimpleUserList, api: () => getSimpleUserList(),
labelField: 'nickname', labelField: 'nickname',
valueField: 'id', valueField: 'id',
}, },

View File

@@ -12,7 +12,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
label: '用户编号', label: '用户编号',
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
api: getSimpleUserList, api: () => getSimpleUserList(),
labelField: 'nickname', labelField: 'nickname',
valueField: 'id', valueField: 'id',
}, },

View File

@@ -93,7 +93,7 @@ export function useFormSchema(): VbenFormSchema[] {
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
placeholder: '请选择引用知识库', placeholder: '请选择引用知识库',
api: getSimpleKnowledgeList, api: () => getSimpleKnowledgeList(),
labelField: 'name', labelField: 'name',
mode: 'multiple', mode: 'multiple',
valueField: 'id', valueField: 'id',
@@ -106,7 +106,7 @@ export function useFormSchema(): VbenFormSchema[] {
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
placeholder: '请选择引用工具', placeholder: '请选择引用工具',
api: getToolSimpleList, api: () => getToolSimpleList(),
mode: 'multiple', mode: 'multiple',
labelField: 'name', labelField: 'name',
valueField: 'id', valueField: 'id',

View File

@@ -48,7 +48,7 @@ export function useFormSchema(): VbenFormSchema[] {
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
placeholder: '请选择API 秘钥', placeholder: '请选择API 秘钥',
api: getApiKeySimpleList, api: () => getApiKeySimpleList(),
labelField: 'name', labelField: 'name',
valueField: 'id', valueField: 'id',
allowClear: true, allowClear: true,

View File

@@ -15,7 +15,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
label: '用户编号', label: '用户编号',
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
api: getSimpleUserList, api: () => getSimpleUserList(),
labelField: 'nickname', labelField: 'nickname',
valueField: 'id', valueField: 'id',
}, },

View File

@@ -15,7 +15,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
label: '用户编号', label: '用户编号',
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
api: getSimpleUserList, api: () => getSimpleUserList(),
labelField: 'nickname', labelField: 'nickname',
valueField: 'id', valueField: 'id',
}, },

View File

@@ -42,7 +42,7 @@ export function useFormSchema(): VbenFormSchema[] {
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
placeholder: '请选择成员', placeholder: '请选择成员',
api: getSimpleUserList, api: () => getSimpleUserList(),
labelField: 'nickname', labelField: 'nickname',
valueField: 'id', valueField: 'id',
mode: 'tags', mode: 'tags',

View File

@@ -17,7 +17,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
// componentProps: { // componentProps: {
// placeholder: '请选择发起人', // placeholder: '请选择发起人',
// allowClear: true, // allowClear: true,
// api: getSimpleUserList, // api: () => getSimpleUserList(),
// labelField: 'nickname', // labelField: 'nickname',
// valueField: 'id', // valueField: 'id',
// }, // },
@@ -48,7 +48,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
componentProps: { componentProps: {
placeholder: '请输入流程分类', placeholder: '请输入流程分类',
allowClear: true, allowClear: true,
api: getCategorySimpleList, api: () => getCategorySimpleList(),
labelField: 'name', labelField: 'name',
valueField: 'code', valueField: 'code',
}, },

View File

@@ -23,7 +23,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
componentProps: { componentProps: {
placeholder: '请选择发起人', placeholder: '请选择发起人',
allowClear: true, allowClear: true,
api: getSimpleUserList, api: () => getSimpleUserList(),
labelField: 'nickname', labelField: 'nickname',
valueField: 'id', valueField: 'id',
}, },
@@ -54,7 +54,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
componentProps: { componentProps: {
placeholder: '请输入流程分类', placeholder: '请输入流程分类',
allowClear: true, allowClear: true,
api: getCategorySimpleList, api: () => getCategorySimpleList(),
labelField: 'name', labelField: 'name',
valueField: 'code', valueField: 'code',
}, },

View File

@@ -35,7 +35,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
componentProps: { componentProps: {
placeholder: '请输入流程分类', placeholder: '请输入流程分类',
allowClear: true, allowClear: true,
api: getCategorySimpleList, api: () => getCategorySimpleList(),
labelField: 'name', labelField: 'name',
valueField: 'code', valueField: 'code',
}, },

View File

@@ -35,7 +35,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
componentProps: { componentProps: {
placeholder: '请输入流程分类', placeholder: '请输入流程分类',
allowClear: true, allowClear: true,
api: getCategorySimpleList, api: () => getCategorySimpleList(),
labelField: 'name', labelField: 'name',
valueField: 'code', valueField: 'code',
}, },

View File

@@ -41,10 +41,8 @@ export function useFormSchema(): VbenFormSchema[] {
}, },
componentProps: { componentProps: {
api: () => getSimpleUserList(), api: () => getSimpleUserList(),
fieldNames: { labelField: 'nickname',
label: 'nickname', valueField: 'id',
value: 'id',
},
placeholder: '请选择负责人', placeholder: '请选择负责人',
allowClear: true, allowClear: true,
}, },
@@ -57,10 +55,8 @@ export function useFormSchema(): VbenFormSchema[] {
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
api: () => getCustomerSimpleList(), api: () => getCustomerSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
placeholder: '请选择客户', placeholder: '请选择客户',
allowClear: true, allowClear: true,
}, },
@@ -85,10 +81,8 @@ export function useFormSchema(): VbenFormSchema[] {
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
api: () => getBusinessStatusTypeSimpleList(), api: () => getBusinessStatusTypeSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
placeholder: '请选择商机状态组', placeholder: '请选择商机状态组',
allowClear: true, allowClear: true,
}, },

View File

@@ -1,11 +1,15 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { Ref } from 'vue'; import type { Ref } from 'vue';
import type { VbenFormSchema } from '#/adapter/form';
import type { CrmBusinessApi } from '#/api/crm/business'; import type { CrmBusinessApi } from '#/api/crm/business';
import type { DescriptionItemSchema } from '#/components/description'; import type { DescriptionItemSchema } from '#/components/description';
import { erpPriceInputFormatter, formatDateTime } from '@vben/utils'; import { erpPriceInputFormatter, formatDateTime } from '@vben/utils';
import { DEFAULT_STATUSES, getBusinessStatusSimpleList } from '#/api/crm/business/status'; import {
DEFAULT_STATUSES,
getBusinessStatusSimpleList,
} from '#/api/crm/business/status';
/** 详情页的字段 */ /** 详情页的字段 */
export function useDetailSchema(): DescriptionItemSchema[] { export function useDetailSchema(): DescriptionItemSchema[] {

View File

@@ -53,7 +53,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '负责人', label: '负责人',
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
api: getSimpleUserList, api: () => getSimpleUserList(),
labelField: 'nickname', labelField: 'nickname',
valueField: 'id', valueField: 'id',
allowClear: true, allowClear: true,

View File

@@ -42,10 +42,8 @@ export function useFormSchema(): VbenFormSchema[] {
}, },
componentProps: { componentProps: {
api: () => getSimpleUserList(), api: () => getSimpleUserList(),
fieldNames: { labelField: 'nickname',
label: 'nickname', valueField: 'id',
value: 'id',
},
placeholder: '请选择负责人', placeholder: '请选择负责人',
}, },
defaultValue: userStore.userInfo?.id, defaultValue: userStore.userInfo?.id,
@@ -57,10 +55,8 @@ export function useFormSchema(): VbenFormSchema[] {
rules: 'required', rules: 'required',
componentProps: { componentProps: {
api: () => getCustomerSimpleList(), api: () => getCustomerSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
placeholder: '请选择客户', placeholder: '请选择客户',
}, },
}, },
@@ -139,10 +135,8 @@ export function useFormSchema(): VbenFormSchema[] {
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
api: () => getSimpleContactList(), api: () => getSimpleContactList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
placeholder: '请选择直属上级', placeholder: '请选择直属上级',
}, },
}, },
@@ -195,10 +189,8 @@ export function useGridFormSchema(): VbenFormSchema[] {
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
api: () => getCustomerSimpleList(), api: () => getCustomerSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
placeholder: '请选择客户', placeholder: '请选择客户',
}, },
}, },

View File

@@ -47,10 +47,8 @@ export function useFormSchema(): VbenFormSchema[] {
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
api: () => getSimpleUserList(), api: () => getSimpleUserList(),
fieldNames: { labelField: 'nickname',
label: 'nickname', valueField: 'id',
value: 'id',
},
}, },
dependencies: { dependencies: {
triggerFields: ['id'], triggerFields: ['id'],
@@ -66,10 +64,8 @@ export function useFormSchema(): VbenFormSchema[] {
rules: 'required', rules: 'required',
componentProps: { componentProps: {
api: () => getCustomerSimpleList(), api: () => getCustomerSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
placeholder: '请选择客户', placeholder: '请选择客户',
}, },
}, },
@@ -142,10 +138,8 @@ export function useFormSchema(): VbenFormSchema[] {
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
api: () => getSimpleUserList(), api: () => getSimpleUserList(),
fieldNames: { labelField: 'nickname',
label: 'nickname', valueField: 'id',
value: 'id',
},
}, },
defaultValue: userStore.userInfo?.id, defaultValue: userStore.userInfo?.id,
}, },
@@ -270,10 +264,8 @@ export function useGridFormSchema(): VbenFormSchema[] {
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
api: () => getCustomerSimpleList(), api: () => getCustomerSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
placeholder: '请选择客户', placeholder: '请选择客户',
allowClear: true, allowClear: true,
}, },

View File

@@ -61,10 +61,8 @@ export function useFormSchema(): VbenFormSchema[] {
}, },
componentProps: { componentProps: {
api: () => getSimpleUserList(), api: () => getSimpleUserList(),
fieldNames: { labelField: 'nickname',
label: 'nickname', valueField: 'id',
value: 'id',
},
placeholder: '请选择负责人', placeholder: '请选择负责人',
allowClear: true, allowClear: true,
}, },
@@ -223,10 +221,8 @@ export function useImportFormSchema(): VbenFormSchema[] {
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
api: () => getSimpleUserList(), api: () => getSimpleUserList(),
fieldNames: { labelField: 'nickname',
label: 'nickname', valueField: 'id',
value: 'id',
},
placeholder: '请选择负责人', placeholder: '请选择负责人',
allowClear: true, allowClear: true,
class: 'w-full', class: 'w-full',

View File

@@ -28,10 +28,8 @@ export function useDistributeFormSchema(): VbenFormSchema[] {
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
api: () => getSimpleUserList(), api: () => getSimpleUserList(),
fieldNames: { labelField: 'nickname',
label: 'nickname', valueField: 'id',
value: 'id',
},
}, },
defaultValue: userStore.userInfo?.id, defaultValue: userStore.userInfo?.id,
rules: 'required', rules: 'required',

View File

@@ -34,11 +34,9 @@ export function useFormSchema(confType: LimitConfType): VbenFormSchema[] {
label: '规则适用人群', label: '规则适用人群',
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
api: getSimpleUserList, api: () => getSimpleUserList(),
fieldNames: { labelField: 'nickname',
label: 'nickname', valueField: 'id',
value: 'id',
},
mode: 'multiple', mode: 'multiple',
allowClear: true, allowClear: true,
placeholder: '请选择规则适用人群', placeholder: '请选择规则适用人群',

View File

@@ -85,8 +85,9 @@ export function useFormSchema(
}); });
return res.list; return res.list;
}, },
labelField: 'name',
valueField: 'id',
mode: 'multiple', mode: 'multiple',
fieldNames: { label: 'name', value: 'id' },
}, },
}, },
{ {
@@ -105,8 +106,9 @@ export function useFormSchema(
}); });
return res.list; return res.list;
}, },
labelField: 'name',
valueField: 'id',
mode: 'multiple', mode: 'multiple',
fieldNames: { label: 'name', value: 'id' },
}, },
}, },
]; ];

View File

@@ -23,7 +23,7 @@ export function useTransferFormSchema(): VbenFormSchema[] {
label: '选择新负责人', label: '选择新负责人',
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
api: getSimpleUserList, api: () => getSimpleUserList(),
labelField: 'nickname', labelField: 'nickname',
valueField: 'id', valueField: 'id',
}, },
@@ -116,7 +116,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '选择人员', label: '选择人员',
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
api: getSimpleUserList, api: () => getSimpleUserList(),
labelField: 'nickname', labelField: 'nickname',
valueField: 'id', valueField: 'id',
}, },

View File

@@ -43,10 +43,8 @@ export function useFormSchema(): VbenFormSchema[] {
}, },
componentProps: { componentProps: {
api: () => getSimpleUserList(), api: () => getSimpleUserList(),
fieldNames: { labelField: 'nickname',
label: 'nickname', valueField: 'id',
value: 'id',
},
placeholder: '请选择负责人', placeholder: '请选择负责人',
allowClear: true, allowClear: true,
}, },

View File

@@ -70,4 +70,3 @@ export function useDetailBaseSchema(): DescriptionItemSchema[] {
}, },
]; ];
} }

View File

@@ -45,10 +45,8 @@ export function useFormSchema(): VbenFormSchema[] {
}, },
componentProps: { componentProps: {
api: () => getSimpleUserList(), api: () => getSimpleUserList(),
fieldNames: { labelField: 'nickname',
label: 'nickname', valueField: 'id',
value: 'id',
},
placeholder: '请选择负责人', placeholder: '请选择负责人',
allowClear: true, allowClear: true,
}, },
@@ -61,10 +59,8 @@ export function useFormSchema(): VbenFormSchema[] {
rules: 'required', rules: 'required',
componentProps: { componentProps: {
api: () => getCustomerSimpleList(), api: () => getCustomerSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
placeholder: '请选择客户', placeholder: '请选择客户',
}, },
dependencies: { dependencies: {
@@ -193,10 +189,8 @@ export function useGridFormSchema(): VbenFormSchema[] {
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
api: () => getCustomerSimpleList(), api: () => getCustomerSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
placeholder: '请选择客户', placeholder: '请选择客户',
allowClear: true, allowClear: true,
}, },

View File

@@ -29,10 +29,8 @@ export function useFormSchema(): VbenFormSchema[] {
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
api: () => getSimpleUserList(), api: () => getSimpleUserList(),
fieldNames: { labelField: 'nickname',
label: 'nickname', valueField: 'id',
value: 'id',
},
}, },
dependencies: { dependencies: {
triggerFields: ['id'], triggerFields: ['id'],
@@ -48,11 +46,10 @@ export function useFormSchema(): VbenFormSchema[] {
rules: 'required', rules: 'required',
componentProps: { componentProps: {
api: () => getCustomerSimpleList(), api: () => getCustomerSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
placeholder: '请选择客户', placeholder: '请选择客户',
allowClear: true,
}, },
}, },
{ {
@@ -63,6 +60,7 @@ export function useFormSchema(): VbenFormSchema[] {
componentProps: { componentProps: {
options: [], options: [],
placeholder: '请选择合同', placeholder: '请选择合同',
allowClear: true,
}, },
dependencies: { dependencies: {
triggerFields: ['customerId'], triggerFields: ['customerId'],
@@ -156,10 +154,8 @@ export function useGridFormSchema(): VbenFormSchema[] {
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
api: () => getCustomerSimpleList(), api: () => getCustomerSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
placeholder: '请选择客户', placeholder: '请选择客户',
allowClear: true, allowClear: true,
}, },

View File

@@ -31,8 +31,7 @@ export function useDetailSchema(): DescriptionItemSchema[] {
{ {
field: 'receivable', field: 'receivable',
label: '实际回款金额', label: '实际回款金额',
content: (data) => content: (data) => erpPriceInputFormatter(data?.receivable?.price ?? 0),
erpPriceInputFormatter(data?.receivable?.price ?? 0),
}, },
]; ];
} }
@@ -78,8 +77,7 @@ export function useDetailBaseSchema(): DescriptionItemSchema[] {
{ {
field: 'receivable', field: 'receivable',
label: '实际回款金额', label: '实际回款金额',
content: (data) => content: (data) => erpPriceInputFormatter(data?.receivable?.price ?? 0),
erpPriceInputFormatter(data?.receivable?.price ?? 0),
}, },
{ {
field: 'receivableRemain', field: 'receivableRemain',
@@ -92,8 +90,7 @@ export function useDetailBaseSchema(): DescriptionItemSchema[] {
{ {
field: 'receivable.returnTime', field: 'receivable.returnTime',
label: '实际回款日期', label: '实际回款日期',
content: (data) => content: (data) => formatDateTime(data?.receivable?.returnTime) as string,
formatDateTime(data?.receivable?.returnTime) as string,
}, },
{ {
field: 'remark', field: 'remark',

View File

@@ -99,7 +99,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
label: '员工', label: '员工',
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
api: getSimpleUserList, api: () => getSimpleUserList(),
labelField: 'nickname', labelField: 'nickname',
valueField: 'id', valueField: 'id',
placeholder: '请选择员工', placeholder: '请选择员工',

View File

@@ -73,7 +73,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
label: '员工', label: '员工',
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
api: getSimpleUserList, api: () => getSimpleUserList(),
allowClear: true, allowClear: true,
labelField: 'nickname', labelField: 'nickname',
valueField: 'id', valueField: 'id',

View File

@@ -64,7 +64,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
label: '员工', label: '员工',
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
api: getSimpleUserList, api: () => getSimpleUserList(),
labelField: 'nickname', labelField: 'nickname',
valueField: 'id', valueField: 'id',
placeholder: '请选择员工', placeholder: '请选择员工',

View File

@@ -68,7 +68,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
label: '员工', label: '员工',
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
api: getSimpleUserList, api: () => getSimpleUserList(),
labelField: 'nickname', labelField: 'nickname',
valueField: 'id', valueField: 'id',
placeholder: '请选择员工', placeholder: '请选择员工',

View File

@@ -38,8 +38,12 @@ const [Grid, gridApi] = useVbenVxeGrid({
ajax: { ajax: {
query: async (_, formValues) => { query: async (_, formValues) => {
const res = await getDatas(activeTabName.value, formValues); const res = await getDatas(activeTabName.value, formValues);
await renderLeftEcharts(getChartOptions(activeTabName.value, res).left); await renderLeftEcharts(
await renderRightEcharts(getChartOptions(activeTabName.value, res).right); getChartOptions(activeTabName.value, res).left,
);
await renderRightEcharts(
getChartOptions(activeTabName.value, res).right,
);
return res; return res;
}, },
}, },

View File

@@ -52,11 +52,9 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
placeholder: '请选择供应商', placeholder: '请选择供应商',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getSupplierSimpleList, api: () => getSupplierSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
}, },
rules: 'required', rules: 'required',
}, },
@@ -68,11 +66,9 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
placeholder: '请选择财务人员', placeholder: '请选择财务人员',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getSimpleUserList, api: () => getSimpleUserList(),
fieldNames: { labelField: 'nickname',
label: 'nickname', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {
@@ -123,11 +119,9 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
placeholder: '请选择付款账户', placeholder: '请选择付款账户',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getAccountSimpleList, api: () => getAccountSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {
@@ -247,11 +241,9 @@ export function useGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择供应商', placeholder: '请选择供应商',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getSupplierSimpleList, api: () => getSupplierSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {
@@ -262,11 +254,9 @@ export function useGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择创建人', placeholder: '请选择创建人',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getSimpleUserList, api: () => getSimpleUserList(),
fieldNames: { labelField: 'nickname',
label: 'nickname', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {
@@ -277,11 +267,9 @@ export function useGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择财务人员', placeholder: '请选择财务人员',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getSimpleUserList, api: () => getSimpleUserList(),
fieldNames: { labelField: 'nickname',
label: 'nickname', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {
@@ -292,11 +280,9 @@ export function useGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择付款账户', placeholder: '请选择付款账户',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getAccountSimpleList, api: () => getAccountSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {

View File

@@ -52,11 +52,9 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
placeholder: '请选择客户', placeholder: '请选择客户',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getCustomerSimpleList, api: () => getCustomerSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
}, },
rules: 'required', rules: 'required',
}, },
@@ -68,11 +66,9 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
placeholder: '请选择财务人员', placeholder: '请选择财务人员',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getSimpleUserList, api: () => getSimpleUserList(),
fieldNames: { labelField: 'nickname',
label: 'nickname', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {
@@ -123,11 +119,9 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
placeholder: '请选择收款账户', placeholder: '请选择收款账户',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getAccountSimpleList, api: () => getAccountSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {
@@ -247,11 +241,9 @@ export function useGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择客户', placeholder: '请选择客户',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getCustomerSimpleList, api: () => getCustomerSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {
@@ -262,11 +254,9 @@ export function useGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择创建人', placeholder: '请选择创建人',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getSimpleUserList, api: () => getSimpleUserList(),
fieldNames: { labelField: 'nickname',
label: 'nickname', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {
@@ -277,11 +267,9 @@ export function useGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择财务人员', placeholder: '请选择财务人员',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getSimpleUserList, api: () => getSimpleUserList(),
fieldNames: { labelField: 'nickname',
label: 'nickname', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {
@@ -292,11 +280,9 @@ export function useGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择收款账户', placeholder: '请选择收款账户',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getAccountSimpleList, api: () => getAccountSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {

View File

@@ -26,17 +26,18 @@ const formData = ref<
} }
>({ >({
id: undefined, id: undefined,
no: undefined, no: '',
customerId: undefined, customerId: 0,
accountId: undefined, accountId: 0,
financeUserId: undefined, financeUserId: 0,
receiptTime: undefined, receiptTime: new Date(),
remark: undefined, remark: '',
fileUrl: undefined, fileUrl: undefined,
totalPrice: 0, totalPrice: 0,
discountPrice: 0, discountPrice: 0,
receiptPrice: 0, receiptPrice: 0,
items: [], items: [],
status: 0,
}); });
const formType = ref(''); // 表单类型:'create' | 'edit' | 'detail' const formType = ref(''); // 表单类型:'create' | 'edit' | 'detail'
@@ -141,7 +142,21 @@ const [Modal, modalApi] = useVbenModal({
}, },
async onOpenChange(isOpen: boolean) { async onOpenChange(isOpen: boolean) {
if (!isOpen) { if (!isOpen) {
formData.value = undefined; formData.value = {
id: undefined,
no: '',
customerId: 0,
accountId: 0,
financeUserId: 0,
receiptTime: new Date(),
remark: '',
totalPrice: 0,
discountPrice: 0,
receiptPrice: 0,
status: 0,
items: [],
bizNo: '',
};
return; return;
} }
// 加载数据 // 加载数据

View File

@@ -3,6 +3,7 @@ import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { CommonStatusEnum, DICT_TYPE } from '@vben/constants'; import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks'; import { getDictOptions } from '@vben/hooks';
import { handleTree } from '@vben/utils';
import { z } from '#/adapter/form'; import { z } from '#/adapter/form';
import { getProductCategorySimpleList } from '#/api/erp/product/category'; import { getProductCategorySimpleList } from '#/api/erp/product/category';
@@ -60,7 +61,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '单位', label: '单位',
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
api: getProductUnitSimpleList, api: () => getProductUnitSimpleList(),
labelField: 'name', labelField: 'name',
valueField: 'id', valueField: 'id',
placeholder: '请选择单位', placeholder: '请选择单位',

View File

@@ -66,11 +66,9 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
placeholder: '请选择供应商', placeholder: '请选择供应商',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getSupplierSimpleList, api: () => getSupplierSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
}, },
rules: 'required', rules: 'required',
}, },
@@ -176,11 +174,9 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
placeholder: '请选择结算账户', placeholder: '请选择结算账户',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getAccountSimpleList, api: () => getAccountSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {
@@ -323,11 +319,9 @@ export function useGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择产品', placeholder: '请选择产品',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getProductSimpleList, api: () => getProductSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {
@@ -347,11 +341,9 @@ export function useGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择供应商', placeholder: '请选择供应商',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getSupplierSimpleList, api: () => getSupplierSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {
@@ -362,7 +354,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择仓库', placeholder: '请选择仓库',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getWarehouseSimpleList, api: () => getWarehouseSimpleList(),
labelField: 'name', labelField: 'name',
valueField: 'id', valueField: 'id',
}, },
@@ -375,11 +367,9 @@ export function useGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择创建人', placeholder: '请选择创建人',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getSimpleUserList, api: () => getSimpleUserList(),
fieldNames: { labelField: 'nickname',
label: 'nickname', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {
@@ -399,11 +389,9 @@ export function useGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择结算账户', placeholder: '请选择结算账户',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getAccountSimpleList, api: () => getAccountSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {
@@ -542,11 +530,9 @@ export function useOrderGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择产品', placeholder: '请选择产品',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getProductSimpleList, api: () => getProductSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {

View File

@@ -52,11 +52,9 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
placeholder: '请选择供应商', placeholder: '请选择供应商',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getSupplierSimpleList, api: () => getSupplierSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
}, },
rules: 'required', rules: 'required',
}, },
@@ -142,11 +140,9 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
placeholder: '请选择结算账户', placeholder: '请选择结算账户',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getAccountSimpleList, api: () => getAccountSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {
@@ -265,11 +261,9 @@ export function useGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择产品', placeholder: '请选择产品',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getProductSimpleList, api: () => getProductSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {
@@ -289,11 +283,9 @@ export function useGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择供应商', placeholder: '请选择供应商',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getSupplierSimpleList, api: () => getSupplierSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {
@@ -304,11 +296,9 @@ export function useGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择创建人', placeholder: '请选择创建人',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getSimpleUserList, api: () => getSimpleUserList(),
fieldNames: { labelField: 'nickname',
label: 'nickname', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {

View File

@@ -66,11 +66,9 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
placeholder: '请选择供应商', placeholder: '请选择供应商',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getSupplierSimpleList, api: () => getSupplierSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
}, },
rules: 'required', rules: 'required',
}, },
@@ -175,11 +173,9 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
placeholder: '请选择结算账户', placeholder: '请选择结算账户',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getAccountSimpleList, api: () => getAccountSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {
@@ -323,11 +319,9 @@ export function useGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择产品', placeholder: '请选择产品',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getProductSimpleList, api: () => getProductSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {
@@ -347,11 +341,9 @@ export function useGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择供应商', placeholder: '请选择供应商',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getSupplierSimpleList, api: () => getSupplierSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {
@@ -362,7 +354,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择仓库', placeholder: '请选择仓库',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getWarehouseSimpleList, api: () => getWarehouseSimpleList(),
labelField: 'name', labelField: 'name',
valueField: 'id', valueField: 'id',
}, },
@@ -375,11 +367,9 @@ export function useGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择创建人', placeholder: '请选择创建人',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getSimpleUserList, api: () => getSimpleUserList(),
fieldNames: { labelField: 'nickname',
label: 'nickname', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {
@@ -526,11 +516,9 @@ export function useOrderGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择产品', placeholder: '请选择产品',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getProductSimpleList, api: () => getProductSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {

View File

@@ -52,11 +52,9 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
placeholder: '请选择客户', placeholder: '请选择客户',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getCustomerSimpleList, api: () => getCustomerSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
}, },
rules: 'required', rules: 'required',
}, },
@@ -68,11 +66,9 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
placeholder: '请选择销售人员', placeholder: '请选择销售人员',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getSimpleUserList, api: () => getSimpleUserList(),
fieldNames: { labelField: 'nickname',
label: 'nickname', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {
@@ -157,11 +153,9 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
placeholder: '请选择结算账户', placeholder: '请选择结算账户',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getAccountSimpleList, api: () => getAccountSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {
@@ -281,11 +275,9 @@ export function useGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择产品', placeholder: '请选择产品',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getProductSimpleList, api: () => getProductSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {
@@ -305,11 +297,9 @@ export function useGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择客户', placeholder: '请选择客户',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getCustomerSimpleList, api: () => getCustomerSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {
@@ -320,11 +310,9 @@ export function useGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择创建人', placeholder: '请选择创建人',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getSimpleUserList, api: () => getSimpleUserList(),
fieldNames: { labelField: 'nickname',
label: 'nickname', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {

View File

@@ -544,11 +544,9 @@ export function useOrderGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择产品', placeholder: '请选择产品',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getProductSimpleList, api: () => getProductSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {

View File

@@ -66,11 +66,9 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
placeholder: '请选择客户', placeholder: '请选择客户',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getCustomerSimpleList, api: () => getCustomerSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
}, },
rules: 'required', rules: 'required',
}, },
@@ -82,11 +80,9 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
placeholder: '请选择销售人员', placeholder: '请选择销售人员',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getSimpleUserList, api: () => getSimpleUserList(),
fieldNames: { labelField: 'nickname',
label: 'nickname', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {
@@ -191,11 +187,9 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
disabled: true, disabled: true,
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getAccountSimpleList, api: () => getAccountSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {
@@ -339,11 +333,9 @@ export function useGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择产品', placeholder: '请选择产品',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getProductSimpleList, api: () => getProductSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {
@@ -363,11 +355,9 @@ export function useGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择客户', placeholder: '请选择客户',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getCustomerSimpleList, api: () => getCustomerSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {
@@ -378,7 +368,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择仓库', placeholder: '请选择仓库',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getWarehouseSimpleList, api: () => getWarehouseSimpleList(),
labelField: 'name', labelField: 'name',
valueField: 'id', valueField: 'id',
}, },
@@ -391,11 +381,9 @@ export function useGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择创建人', placeholder: '请选择创建人',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getSimpleUserList, api: () => getSimpleUserList(),
fieldNames: { labelField: 'nickname',
label: 'nickname', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {
@@ -543,11 +531,9 @@ export function useOrderGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择产品', placeholder: '请选择产品',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getProductSimpleList, api: () => getProductSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {

View File

@@ -180,11 +180,9 @@ export function useGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择产品', placeholder: '请选择产品',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getProductSimpleList, api: () => getProductSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {
@@ -204,11 +202,9 @@ export function useGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择仓库', placeholder: '请选择仓库',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getWarehouseSimpleList, api: () => getWarehouseSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {
@@ -219,11 +215,9 @@ export function useGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择创建人', placeholder: '请选择创建人',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getSimpleUserList, api: () => getSimpleUserList(),
fieldNames: { labelField: 'nickname',
label: 'nickname', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {

View File

@@ -50,11 +50,9 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
placeholder: '请选择供应商', placeholder: '请选择供应商',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getSupplierSimpleList, api: () => getSupplierSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
}, },
rules: 'required', rules: 'required',
}, },
@@ -189,11 +187,9 @@ export function useGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择产品', placeholder: '请选择产品',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getProductSimpleList, api: () => getProductSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {
@@ -213,11 +209,9 @@ export function useGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择供应商', placeholder: '请选择供应商',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getSupplierSimpleList, api: () => getSupplierSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {
@@ -228,11 +222,9 @@ export function useGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择仓库', placeholder: '请选择仓库',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getWarehouseSimpleList, api: () => getWarehouseSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {
@@ -243,11 +235,9 @@ export function useGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择创建人', placeholder: '请选择创建人',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getSimpleUserList, api: () => getSimpleUserList(),
fieldNames: { labelField: 'nickname',
label: 'nickname', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {

View File

@@ -178,11 +178,9 @@ export function useGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择产品', placeholder: '请选择产品',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getProductSimpleList, api: () => getProductSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {
@@ -202,11 +200,9 @@ export function useGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择调出仓库', placeholder: '请选择调出仓库',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getWarehouseSimpleList, api: () => getWarehouseSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {
@@ -217,11 +213,9 @@ export function useGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择调入仓库', placeholder: '请选择调入仓库',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getWarehouseSimpleList, api: () => getWarehouseSimpleList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {
@@ -232,11 +226,9 @@ export function useGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择创建人', placeholder: '请选择创建人',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getSimpleUserList, api: () => getSimpleUserList(),
fieldNames: { labelField: 'nickname',
label: 'nickname', valueField: 'id',
value: 'id',
},
}, },
}, },
{ {

View File

@@ -19,7 +19,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择产品', placeholder: '请选择产品',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getProductSimpleList, api: () => getProductSimpleList(),
labelField: 'name', labelField: 'name',
valueField: 'id', valueField: 'id',
}, },
@@ -32,7 +32,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择仓库', placeholder: '请选择仓库',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getWarehouseSimpleList, api: () => getWarehouseSimpleList(),
labelField: 'name', labelField: 'name',
valueField: 'id', valueField: 'id',
}, },

View File

@@ -15,7 +15,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择产品', placeholder: '请选择产品',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getProductSimpleList, api: () => getProductSimpleList(),
labelField: 'name', labelField: 'name',
valueField: 'id', valueField: 'id',
}, },
@@ -28,7 +28,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择仓库', placeholder: '请选择仓库',
allowClear: true, allowClear: true,
showSearch: true, showSearch: true,
api: getWarehouseSimpleList, api: () => getWarehouseSimpleList(),
labelField: 'name', labelField: 'name',
valueField: 'id', valueField: 'id',
}, },

View File

@@ -25,13 +25,9 @@ export function useImportTableFormSchema(): VbenFormSchema[] {
label: '数据源', label: '数据源',
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
api: async () => { api: () => getDataSourceConfigList(),
const data = await getDataSourceConfigList(); labelField: 'name',
return data.map((item) => ({ valueField: 'id',
label: item.name,
value: item.id,
}));
},
autoSelect: 'first', autoSelect: 'first',
placeholder: '请选择数据源', placeholder: '请选择数据源',
}, },

View File

@@ -63,7 +63,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '关联场景联动规则', label: '关联场景联动规则',
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
api: getSimpleRuleSceneList, api: () => getSimpleRuleSceneList(),
labelField: 'name', labelField: 'name',
valueField: 'id', valueField: 'id',
mode: 'multiple', mode: 'multiple',
@@ -76,7 +76,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '接收的用户', label: '接收的用户',
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
api: getSimpleUserList, api: () => getSimpleUserList(),
labelField: 'nickname', labelField: 'nickname',
valueField: 'id', valueField: 'id',
mode: 'multiple', mode: 'multiple',

View File

@@ -17,7 +17,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
label: '告警配置', label: '告警配置',
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
api: getSimpleAlertConfigList, api: () => getSimpleAlertConfigList(),
labelField: 'name', labelField: 'name',
valueField: 'id', valueField: 'id',
placeholder: '请选择告警配置', placeholder: '请选择告警配置',
@@ -40,7 +40,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
label: '产品', label: '产品',
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
api: getSimpleProductList, api: () => getSimpleProductList(),
labelField: 'name', labelField: 'name',
valueField: 'id', valueField: 'id',
placeholder: '请选择产品', placeholder: '请选择产品',
@@ -53,7 +53,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
label: '设备', label: '设备',
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
api: getSimpleDeviceList, api: () => getSimpleDeviceList(),
labelField: 'deviceName', labelField: 'deviceName',
valueField: 'id', valueField: 'id',
placeholder: '请选择设备', placeholder: '请选择设备',

View File

@@ -5,6 +5,7 @@ import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks'; import { getDictOptions } from '@vben/hooks';
import { z } from '#/adapter/form'; import { z } from '#/adapter/form';
import { getSimpleDeviceList } from '#/api/iot/device/device';
import { getSimpleDeviceGroupList } from '#/api/iot/device/group'; import { getSimpleDeviceGroupList } from '#/api/iot/device/group';
import { import {
DeviceTypeEnum, DeviceTypeEnum,
@@ -27,7 +28,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '产品', label: '产品',
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
api: getSimpleProductList, api: () => getSimpleProductList(),
labelField: 'name', labelField: 'name',
valueField: 'id', valueField: 'id',
placeholder: '请选择产品', placeholder: '请选择产品',
@@ -55,12 +56,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '网关设备', label: '网关设备',
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
api: async () => { api: () => getSimpleDeviceList(DeviceTypeEnum.GATEWAY),
const { getSimpleDeviceList } = await import(
'#/api/iot/device/device'
);
return getSimpleDeviceList(DeviceTypeEnum.GATEWAY);
},
labelField: 'nickname', labelField: 'nickname',
valueField: 'id', valueField: 'id',
placeholder: '子设备可选择父设备', placeholder: '子设备可选择父设备',
@@ -93,7 +89,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '设备分组', label: '设备分组',
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
api: getSimpleDeviceGroupList, api: () => getSimpleDeviceGroupList(),
labelField: 'name', labelField: 'name',
valueField: 'id', valueField: 'id',
mode: 'multiple', mode: 'multiple',
@@ -113,16 +109,16 @@ export function useFormSchema(): VbenFormSchema[] {
.optional() .optional()
.or(z.literal('')), .or(z.literal('')),
}, },
// { {
// fieldName: 'locationType', fieldName: 'locationType',
// label: '定位类型', label: '定位类型',
// component: 'RadioGroup', component: 'RadioGroup',
// componentProps: { componentProps: {
// options: getDictOptions(DICT_TYPE.IOT_LOCATION_TYPE, 'number'), options: getDictOptions(DICT_TYPE.IOT_LOCATION_TYPE, 'number'),
// buttonStyle: 'solid', buttonStyle: 'solid',
// optionType: 'button', optionType: 'button',
// }, },
// }, },
{ {
fieldName: 'longitude', fieldName: 'longitude',
label: '设备经度', label: '设备经度',
@@ -160,7 +156,7 @@ export function useGroupFormSchema(): VbenFormSchema[] {
label: '设备分组', label: '设备分组',
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
api: getSimpleDeviceGroupList, api: () => getSimpleDeviceGroupList(),
labelField: 'name', labelField: 'name',
valueField: 'id', valueField: 'id',
mode: 'multiple', mode: 'multiple',
@@ -203,7 +199,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
label: '产品', label: '产品',
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
api: getSimpleProductList, api: () => getSimpleProductList(),
labelField: 'name', labelField: 'name',
valueField: 'id', valueField: 'id',
placeholder: '请选择产品', placeholder: '请选择产品',
@@ -253,7 +249,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
label: '设备分组', label: '设备分组',
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
api: getSimpleDeviceGroupList, api: () => getSimpleDeviceGroupList(),
labelField: 'name', labelField: 'name',
valueField: 'id', valueField: 'id',
placeholder: '请选择设备分组', placeholder: '请选择设备分组',

View File

@@ -32,7 +32,6 @@ import { getSimpleProductList } from '#/api/iot/product/product';
import { $t } from '#/locales'; import { $t } from '#/locales';
import { useGridColumns } from './data'; import { useGridColumns } from './data';
// @ts-ignore
import DeviceCardView from './modules/DeviceCardView.vue'; import DeviceCardView from './modules/DeviceCardView.vue';
import DeviceForm from './modules/DeviceForm.vue'; import DeviceForm from './modules/DeviceForm.vue';
import DeviceGroupForm from './modules/DeviceGroupForm.vue'; import DeviceGroupForm from './modules/DeviceGroupForm.vue';

View File

@@ -44,7 +44,7 @@ const [Modal, modalApi] = useVbenModal({
// 关闭并提示 // 关闭并提示
await modalApi.close(); await modalApi.close();
emit('success'); emit('success');
message.success($t('common.updateSuccess')); message.success($t('ui.actionMessage.operationSuccess'));
} finally { } finally {
modalApi.unlock(); modalApi.unlock();
} }

View File

@@ -111,14 +111,14 @@ const [Modal, modalApi] = useVbenModal({
}); });
/** 下载模板 */ /** 下载模板 */
const handleDownloadTemplate = async () => { async function handleDownloadTemplate() {
try { try {
const res = await importDeviceTemplate(); const res = await importDeviceTemplate();
downloadFileFromBlobPart({ fileName: '设备导入模版.xls', source: res }); downloadFileFromBlobPart({ fileName: '设备导入模版.xls', source: res });
} catch (error: any) { } catch (error: any) {
message.error(error.message || '下载失败'); message.error(error.message || '下载失败');
} }
}; }
</script> </script>
<template> <template>

View File

@@ -139,7 +139,7 @@ const rowSelection = computed(() => ({
})); }));
/** 查询列表 */ /** 查询列表 */
const getList = async () => { async function getList() {
loading.value = true; loading.value = true;
try { try {
if (props.productId) { if (props.productId) {
@@ -151,22 +151,22 @@ const getList = async () => {
} finally { } finally {
loading.value = false; loading.value = false;
} }
}; }
/** 搜索按钮操作 */ /** 搜索按钮操作 */
const handleQuery = () => { function handleQuery() {
queryParams.pageNo = 1; queryParams.pageNo = 1;
getList(); getList();
}; }
/** 重置按钮操作 */ /** 重置按钮操作 */
const resetQuery = () => { function resetQuery() {
queryFormRef.value.resetFields(); queryFormRef.value.resetFields();
handleQuery(); handleQuery();
}; }
/** 打开弹窗 */ /** 打开弹窗 */
const open = async () => { async function open() {
dialogVisible.value = true; dialogVisible.value = true;
// 重置选择状态 // 重置选择状态
selectedDevices.value = []; selectedDevices.value = [];
@@ -178,7 +178,7 @@ const open = async () => {
} }
// 获取设备列表 // 获取设备列表
await getList(); await getList();
}; }
defineExpose({ open }); defineExpose({ open });
/** 处理行点击事件 */ /** 处理行点击事件 */

View File

@@ -2,9 +2,9 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { IotDeviceApi } from '#/api/iot/device/device'; import type { IotDeviceApi } from '#/api/iot/device/device';
import { ref, watchEffect } from 'vue'; import { computed, ref, watchEffect } from 'vue';
import { Alert, Button, message } from 'ant-design-vue'; import { Alert, Button, message, Textarea } from 'ant-design-vue';
import { sendDeviceMessage, updateDevice } from '#/api/iot/device/device'; import { sendDeviceMessage, updateDevice } from '#/api/iot/device/device';
import { IotDeviceMessageMethodEnum } from '#/views/iot/utils/constants'; import { IotDeviceMessageMethodEnum } from '#/views/iot/utils/constants';
@@ -22,41 +22,69 @@ const emit = defineEmits<{
const loading = ref(false); // 加载中 const loading = ref(false); // 加载中
const pushLoading = ref(false); // 推送加载中 const pushLoading = ref(false); // 推送加载中
const config = ref<any>({}); // 只存储 config 字段 const config = ref<any>({}); // 只存储 config 字段
const hasJsonError = ref(false); // 是否有 JSON 格式错误 const configString = ref(''); // 用于编辑器的字符串格式
/** 监听 props.device 的变化,只更新 config 字段 */ /** 监听 props.device 的变化,只更新 config 字段 */
watchEffect(() => { watchEffect(() => {
try { try {
config.value = props.device.config ? JSON.parse(props.device.config) : {}; config.value = props.device.config ? JSON.parse(props.device.config) : {};
// 将对象转换为格式化的 JSON 字符串
configString.value = JSON.stringify(config.value, null, 2);
} catch { } catch {
config.value = {}; config.value = {};
configString.value = '{}';
} }
}); });
const isEditing = ref(false); // 编辑状态 const isEditing = ref(false); // 编辑状态
/** 格式化的配置用于只读展示 */
const formattedConfig = computed(() => {
try {
if (typeof config.value === 'string') {
return JSON.stringify(JSON.parse(config.value), null, 2);
}
return JSON.stringify(config.value, null, 2);
} catch {
return JSON.stringify(config.value, null, 2);
}
});
/** 判断配置是否有数据 */
const hasConfigData = computed(() => {
return config.value && Object.keys(config.value).length > 0;
});
/** 启用编辑模式的函数 */ /** 启用编辑模式的函数 */
function enableEdit() { function enableEdit() {
isEditing.value = true; isEditing.value = true;
hasJsonError.value = false; // 重置错误状态 // 重新同步编辑器内容
configString.value = JSON.stringify(config.value, null, 2);
} }
/** 取消编辑的函数 */ /** 取消编辑的函数 */
function cancelEdit() { function cancelEdit() {
try { try {
config.value = props.device.config ? JSON.parse(props.device.config) : {}; config.value = props.device.config ? JSON.parse(props.device.config) : {};
configString.value = JSON.stringify(config.value, null, 2);
} catch { } catch {
config.value = {}; config.value = {};
configString.value = '{}';
} }
isEditing.value = false; isEditing.value = false;
hasJsonError.value = false; // 重置错误状态
} }
/** 保存配置的函数 */ /** 保存配置的函数 */
async function saveConfig() { async function saveConfig() {
if (hasJsonError.value) { // 验证 JSON 格式
try {
config.value = JSON.parse(configString.value);
} catch (error) {
console.error('JSON格式错误:', error);
message.error({ content: 'JSON格式错误请修正后再提交' }); message.error({ content: 'JSON格式错误请修正后再提交' });
return; return;
} }
await updateDeviceConfig(); await updateDeviceConfig();
isEditing.value = false; isEditing.value = false;
} }
@@ -102,39 +130,26 @@ async function updateDeviceConfig() {
loading.value = false; loading.value = false;
} }
} }
/** 处理 JSON 编辑器错误的函数 */
function onError(errors: any) {
if (!errors || (Array.isArray(errors) && errors.length === 0)) {
hasJsonError.value = false;
return;
}
hasJsonError.value = true;
}
</script> </script>
<template> <template>
<div> <div>
<!-- 只在没有配置数据时显示提示 -->
<Alert <Alert
v-if="!hasConfigData"
message="支持远程更新设备的配置文件(JSON 格式),可以在下方编辑配置模板,对设备的系统参数、网络参数等进行远程配置。配置完成后,需点击「下发」按钮,设备即可进行远程配置。" message="支持远程更新设备的配置文件(JSON 格式),可以在下方编辑配置模板,对设备的系统参数、网络参数等进行远程配置。配置完成后,需点击「下发」按钮,设备即可进行远程配置。"
type="info" type="info"
show-icon show-icon
class="my-4" class="my-4"
description="如需编辑文件,请点击下方编辑按钮" description="如需编辑文件,请点击下方编辑按钮"
/> />
<JsonEditor
v-model="config"
:mode="isEditing ? 'code' : 'view'"
height="600px"
@error="onError"
/>
<div class="mt-5 text-center"> <div class="mt-5 text-center">
<Button v-if="isEditing" @click="cancelEdit">取消</Button> <Button v-if="isEditing" @click="cancelEdit">取消</Button>
<Button <Button
v-if="isEditing" v-if="isEditing"
type="primary" type="primary"
@click="saveConfig" @click="saveConfig"
:disabled="hasJsonError" :loading="loading"
> >
保存 保存
</Button> </Button>
@@ -148,5 +163,45 @@ function onError(errors: any) {
配置推送 配置推送
</Button> </Button>
</div> </div>
<!-- 代码视图 - 只读展示 -->
<div v-if="!isEditing" class="json-viewer-container">
<pre class="json-code"><code>{{ formattedConfig }}</code></pre>
</div>
<!-- 编辑器视图 - 可编辑 -->
<Textarea
v-else
v-model:value="configString"
:rows="20"
placeholder="请输入 JSON 格式的配置信息"
class="json-editor"
/>
</div> </div>
</template> </template>
<style scoped>
.json-viewer-container {
max-height: 600px;
padding: 12px;
overflow-y: auto;
background-color: #f5f5f5;
border: 1px solid #d9d9d9;
border-radius: 4px;
}
.json-code {
margin: 0;
font-family: Monaco, Menlo, 'Ubuntu Mono', Consolas, monospace;
font-size: 13px;
line-height: 1.5;
color: #333;
word-wrap: break-word;
white-space: pre-wrap;
}
.json-editor {
font-family: Monaco, Menlo, 'Ubuntu Mono', Consolas, monospace;
font-size: 13px;
}
</style>

View File

@@ -85,7 +85,7 @@ function handleAuthInfoDialogClose() {
<span>设备信息</span> <span>设备信息</span>
</div> </div>
</template> </template>
<Descriptions :column="1" bordered> <Descriptions :column="1" bordered size="small">
<Descriptions.Item label="产品名称"> <Descriptions.Item label="产品名称">
{{ product.name }} {{ product.name }}
</Descriptions.Item> </Descriptions.Item>

View File

@@ -1,4 +1,3 @@
<!-- 设备消息列表 -->
<script setup lang="ts"> <script setup lang="ts">
import { import {
computed, computed,
@@ -62,36 +61,42 @@ const columns = [
title: '时间', title: '时间',
dataIndex: 'ts', dataIndex: 'ts',
key: 'ts', key: 'ts',
align: 'center' as const,
width: 180, width: 180,
}, },
{ {
title: '上行/下行', title: '上行/下行',
dataIndex: 'upstream', dataIndex: 'upstream',
key: 'upstream', key: 'upstream',
align: 'center' as const,
width: 140, width: 140,
}, },
{ {
title: '是否回复', title: '是否回复',
dataIndex: 'reply', dataIndex: 'reply',
key: 'reply', key: 'reply',
align: 'center' as const,
width: 140, width: 140,
}, },
{ {
title: '请求编号', title: '请求编号',
dataIndex: 'requestId', dataIndex: 'requestId',
key: 'requestId', key: 'requestId',
align: 'center' as const,
width: 300, width: 300,
}, },
{ {
title: '请求方法', title: '请求方法',
dataIndex: 'method', dataIndex: 'method',
key: 'method', key: 'method',
align: 'center' as const,
width: 140, width: 140,
}, },
{ {
title: '请求/响应数据', title: '请求/响应数据',
dataIndex: 'params', dataIndex: 'params',
key: 'params', key: 'params',
align: 'center' as const,
ellipsis: true, ellipsis: true,
}, },
]; ];
@@ -220,7 +225,6 @@ defineExpose({
:data-source="list" :data-source="list"
:columns="columns" :columns="columns"
:pagination="false" :pagination="false"
align="center"
class="whitespace-nowrap" class="whitespace-nowrap"
> >
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">

View File

@@ -8,19 +8,21 @@ import type { ThingModelData } from '#/api/iot/thingmodel';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { ContentWrap } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
import { import {
Button, Button,
Card, Card,
Col,
Input, Input,
message, message,
Row,
Table, Table,
Tabs, Tabs,
Textarea, Textarea,
} from 'ant-design-vue'; } from 'ant-design-vue';
import { DeviceStateEnum, sendDeviceMessage } from '#/api/iot/device/device'; import { DeviceStateEnum, sendDeviceMessage } from '#/api/iot/device/device';
import DataDefinition from '#/views/iot/thingmodel/modules/components/DataDefinition.vue';
import { import {
IotDeviceMessageMethodEnum, IotDeviceMessageMethodEnum,
IoTThingModelTypeEnum, IoTThingModelTypeEnum,
@@ -41,6 +43,10 @@ const downstreamTab = ref(IotDeviceMessageMethodEnum.PROPERTY_SET.method); //
const deviceMessageRef = ref(); // 设备消息组件引用 const deviceMessageRef = ref(); // 设备消息组件引用
const deviceMessageRefreshDelay = 2000; // 延迟 N 秒,保证模拟上行的消息被处理 const deviceMessageRefreshDelay = 2000; // 延迟 N 秒,保证模拟上行的消息被处理
// 折叠状态
const debugCollapsed = ref(false); // 指令调试区域折叠状态
const messageCollapsed = ref(false); // 设备消息区域折叠状态
// 表单数据:存储用户输入的模拟值 // 表单数据:存储用户输入的模拟值
const formData = ref<Record<string, string>>({}); const formData = ref<Record<string, string>>({});
@@ -90,12 +96,12 @@ const propertyColumns: TableColumnType[] = [
{ {
title: '数据定义', title: '数据定义',
key: 'dataDefinition', key: 'dataDefinition',
minWidth: 200, minWidth: 100,
}, },
{ {
title: '值', title: '值',
key: 'value', key: 'value',
width: 150, width: 300,
fixed: 'right' as any, fixed: 'right' as any,
}, },
]; ];
@@ -332,19 +338,32 @@ async function handleServiceInvoke(row: ThingModelData) {
<template> <template>
<ContentWrap> <ContentWrap>
<Row :gutter="20"> <!-- 上方指令调试区域 -->
<!-- 左侧指令调试区域 --> <Card class="simulator-tabs mb-4">
<Col :span="12"> <template #title>
<Card> <div class="flex items-center justify-between">
<Tabs v-model:active-key="activeTab"> <span>指令调试</span>
<Button
type="text"
size="small"
@click="debugCollapsed = !debugCollapsed"
>
<IconifyIcon icon="lucide:chevron-up" v-if="!debugCollapsed" />
<IconifyIcon icon="lucide:chevron-down" v-if="debugCollapsed" />
</Button>
</div>
</template>
<div v-show="!debugCollapsed">
<Tabs v-model:active-key="activeTab" size="small">
<!-- 上行指令调试 --> <!-- 上行指令调试 -->
<Tabs.Pane key="upstream" tab="上行指令调试"> <Tabs.TabPane key="upstream" tab="上行指令调试">
<Tabs <Tabs
v-if="activeTab === 'upstream'" v-if="activeTab === 'upstream'"
v-model:active-key="upstreamTab" v-model:active-key="upstreamTab"
size="small"
> >
<!-- 属性上报 --> <!-- 属性上报 -->
<Tabs.Pane <Tabs.TabPane
:key="IotDeviceMessageMethodEnum.PROPERTY_POST.method" :key="IotDeviceMessageMethodEnum.PROPERTY_POST.method"
tab="属性上报" tab="属性上报"
> >
@@ -354,6 +373,8 @@ async function handleServiceInvoke(row: ThingModelData) {
align="center" align="center"
:columns="propertyColumns" :columns="propertyColumns"
:pagination="false" :pagination="false"
:scroll="{ y: 300 }"
size="small"
> >
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.key === 'dataType'"> <template v-if="column.key === 'dataType'">
@@ -383,10 +404,10 @@ async function handleServiceInvoke(row: ThingModelData) {
</Button> </Button>
</div> </div>
</ContentWrap> </ContentWrap>
</Tabs.Pane> </Tabs.TabPane>
<!-- 事件上报 --> <!-- 事件上报 -->
<Tabs.Pane <Tabs.TabPane
:key="IotDeviceMessageMethodEnum.EVENT_POST.method" :key="IotDeviceMessageMethodEnum.EVENT_POST.method"
tab="事件上报" tab="事件上报"
> >
@@ -396,6 +417,8 @@ async function handleServiceInvoke(row: ThingModelData) {
align="center" align="center"
:columns="eventColumns" :columns="eventColumns"
:pagination="false" :pagination="false"
:scroll="{ y: 300 }"
size="small"
> >
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.key === 'dataType'"> <template v-if="column.key === 'dataType'">
@@ -427,10 +450,10 @@ async function handleServiceInvoke(row: ThingModelData) {
</template> </template>
</Table> </Table>
</ContentWrap> </ContentWrap>
</Tabs.Pane> </Tabs.TabPane>
<!-- 状态变更 --> <!-- 状态变更 -->
<Tabs.Pane <Tabs.TabPane
:key="IotDeviceMessageMethodEnum.STATE_UPDATE.method" :key="IotDeviceMessageMethodEnum.STATE_UPDATE.method"
tab="状态变更" tab="状态变更"
> >
@@ -450,18 +473,19 @@ async function handleServiceInvoke(row: ThingModelData) {
</Button> </Button>
</div> </div>
</ContentWrap> </ContentWrap>
</Tabs.Pane> </Tabs.TabPane>
</Tabs> </Tabs>
</Tabs.Pane> </Tabs.TabPane>
<!-- 下行指令调试 --> <!-- 下行指令调试 -->
<Tabs.Pane key="downstream" tab="下行指令调试"> <Tabs.TabPane key="downstream" tab="下行指令调试">
<Tabs <Tabs
v-if="activeTab === 'downstream'" v-if="activeTab === 'downstream'"
v-model:active-key="downstreamTab" v-model:active-key="downstreamTab"
size="small"
> >
<!-- 属性调试 --> <!-- 属性调试 -->
<Tabs.Pane <Tabs.TabPane
:key="IotDeviceMessageMethodEnum.PROPERTY_SET.method" :key="IotDeviceMessageMethodEnum.PROPERTY_SET.method"
tab="属性设置" tab="属性设置"
> >
@@ -471,6 +495,8 @@ async function handleServiceInvoke(row: ThingModelData) {
align="center" align="center"
:columns="propertyColumns" :columns="propertyColumns"
:pagination="false" :pagination="false"
:scroll="{ y: 300 }"
size="small"
> >
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.key === 'dataType'"> <template v-if="column.key === 'dataType'">
@@ -500,10 +526,10 @@ async function handleServiceInvoke(row: ThingModelData) {
</Button> </Button>
</div> </div>
</ContentWrap> </ContentWrap>
</Tabs.Pane> </Tabs.TabPane>
<!-- 服务调用 --> <!-- 服务调用 -->
<Tabs.Pane <Tabs.TabPane
:key="IotDeviceMessageMethodEnum.SERVICE_INVOKE.method" :key="IotDeviceMessageMethodEnum.SERVICE_INVOKE.method"
tab="设备服务调用" tab="设备服务调用"
> >
@@ -513,6 +539,8 @@ async function handleServiceInvoke(row: ThingModelData) {
align="center" align="center"
:columns="serviceColumns" :columns="serviceColumns"
:pagination="false" :pagination="false"
:scroll="{ y: 300 }"
size="small"
> >
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.key === 'dataDefinition'"> <template v-if="column.key === 'dataDefinition'">
@@ -541,23 +569,35 @@ async function handleServiceInvoke(row: ThingModelData) {
</template> </template>
</Table> </Table>
</ContentWrap> </ContentWrap>
</Tabs.Pane> </Tabs.TabPane>
</Tabs> </Tabs>
</Tabs.Pane> </Tabs.TabPane>
</Tabs> </Tabs>
</div>
</Card> </Card>
</Col>
<!-- 右侧设备日志区域 --> <!-- 下方设备消息区域 -->
<Col :span="12"> <Card>
<ContentWrap title="设备消息"> <template #title>
<div class="flex items-center justify-between">
<span>设备消息</span>
<Button
type="text"
size="small"
@click="messageCollapsed = !messageCollapsed"
>
<IconifyIcon icon="lucide:chevron-down" v-if="!messageCollapsed" />
<IconifyIcon icon="lucide:chevron-down" v-if="messageCollapsed" />
</Button>
</div>
</template>
<div v-show="!messageCollapsed">
<DeviceDetailsMessage <DeviceDetailsMessage
v-if="device.id" v-if="device.id"
ref="deviceMessageRef" ref="deviceMessageRef"
:device-id="device.id" :device-id="device.id"
/> />
</ContentWrap> </div>
</Col> </Card>
</Row>
</ContentWrap> </ContentWrap>
</template> </template>

View File

@@ -0,0 +1,43 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { Card, Empty } from 'ant-design-vue';
interface Props {
deviceId: number;
}
defineProps<Props>();
const loading = ref(false);
const subDevices = ref<any[]>([]);
/** 获取子设备列表 */
async function getSubDeviceList() {
loading.value = true;
try {
// TODO: 实现获取子设备列表的API调用
// const data = await getSubDevicesByGatewayId(deviceId);
// subDevices.value = data || [];
subDevices.value = [];
} catch (error) {
console.error('获取子设备列表失败:', error);
} finally {
loading.value = false;
}
}
onMounted(() => {
getSubDeviceList();
});
</script>
<template>
<Card :loading="loading" title="子设备管理">
<Empty
description="暂无子设备数据,此功能待实现"
:image="Empty.PRESENTED_IMAGE_SIMPLE"
/>
<!-- TODO: 实现子设备列表展示和管理功能 -->
</Card>
</template>

View File

@@ -22,21 +22,26 @@ const activeTab = ref('property'); // 默认选中设备属性
<template> <template>
<ContentWrap> <ContentWrap>
<Tabs v-model:active-key="activeTab" class="!h-auto !p-0"> <Tabs v-model:active-key="activeTab" class="!h-auto !p-0">
<Tabs.Pane key="property" tab="设备属性(运行状态)"> <Tabs.TabPane key="property" tab="设备属性(运行状态)">
<DeviceDetailsThingModelProperty :device-id="deviceId" /> <DeviceDetailsThingModelProperty
</Tabs.Pane> v-if="activeTab === 'property'"
<Tabs.Pane key="event" tab="设备事件上报"> :device-id="deviceId"
/>
</Tabs.TabPane>
<Tabs.TabPane key="event" tab="设备事件上报">
<DeviceDetailsThingModelEvent <DeviceDetailsThingModelEvent
v-if="activeTab === 'event'"
:device-id="props.deviceId" :device-id="props.deviceId"
:thing-model-list="props.thingModelList" :thing-model-list="props.thingModelList"
/> />
</Tabs.Pane> </Tabs.TabPane>
<Tabs.Pane key="service" tab="设备服务调用"> <Tabs.TabPane key="service" tab="设备服务调用">
<DeviceDetailsThingModelService <DeviceDetailsThingModelService
v-if="activeTab === 'service'"
:device-id="deviceId" :device-id="deviceId"
:thing-model-list="props.thingModelList" :thing-model-list="props.thingModelList"
/> />
</Tabs.Pane> </Tabs.TabPane>
</Tabs> </Tabs>
</ContentWrap> </ContentWrap>
</template> </template>

View File

@@ -148,6 +148,7 @@ onMounted(() => {
v-model:value="queryParams.times" v-model:value="queryParams.times"
show-time show-time
format="YYYY-MM-DD HH:mm:ss" format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 360px" style="width: 360px"
/> />
</Form.Item> </Form.Item>

View File

@@ -1,4 +1,5 @@
<!-- 设备物模型 -> 运行状态 -> 查看数据设备的属性值历史--> <!-- 设备物模型 -> 运行状态 -> 查看数据设备的属性值历史-->
// 重新关闭打开图表,图表不显示可能图例注销失败等大佬修复
<script setup lang="ts"> <script setup lang="ts">
import type { Dayjs } from 'dayjs'; import type { Dayjs } from 'dayjs';
@@ -10,7 +11,7 @@ import { computed, nextTick, reactive, ref, watch } from 'vue';
import { IconifyIcon } from '@vben/icons'; import { IconifyIcon } from '@vben/icons';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts'; import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
import { beginOfDay, endOfDay, formatDate } from '@vben/utils'; import { beginOfDay, endOfDay, formatDate, formatDateTime } from '@vben/utils';
import { import {
Button, Button,
@@ -20,6 +21,7 @@ import {
RangePicker, RangePicker,
Space, Space,
Spin, Spin,
Table,
Tag, Tag,
} from 'ant-design-vue'; } from 'ant-design-vue';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
@@ -49,8 +51,8 @@ const queryParams = reactive({
deviceId: -1, deviceId: -1,
identifier: '', identifier: '',
times: [ times: [
formatDate(beginOfDay(new Date(Date.now() - 3600 * 1000 * 24 * 7))), formatDateTime(beginOfDay(new Date(Date.now() - 3600 * 1000 * 24 * 7))),
formatDate(endOfDay(new Date())), formatDateTime(endOfDay(new Date())),
], ],
}); });
@@ -100,7 +102,7 @@ const tableColumns = computed(() => [
title: '序号', title: '序号',
key: 'index', key: 'index',
width: 80, width: 80,
align: 'center', align: 'center' as const,
customRender: ({ index }: { index: number }) => index + 1, customRender: ({ index }: { index: number }) => index + 1,
}, },
{ {
@@ -108,20 +110,20 @@ const tableColumns = computed(() => [
key: 'updateTime', key: 'updateTime',
dataIndex: 'updateTime', dataIndex: 'updateTime',
width: 200, width: 200,
align: 'center', align: 'center' as const,
}, },
{ {
title: '属性值', title: '属性值',
key: 'value', key: 'value',
dataIndex: 'value', dataIndex: 'value',
align: 'center', align: 'center' as const,
}, },
]); ]);
// 分页配置 // 分页配置
const paginationConfig = computed(() => ({ const paginationConfig = computed(() => ({
current: 1, current: 1,
pageSize: 20, pageSize: 10,
total: total.value, total: total.value,
showSizeChanger: true, showSizeChanger: true,
showQuickJumper: true, showQuickJumper: true,
@@ -134,7 +136,10 @@ async function getList() {
loading.value = true; loading.value = true;
try { try {
const data = await getHistoryDevicePropertyList(queryParams); const data = await getHistoryDevicePropertyList(queryParams);
list.value = (data?.list as IotDeviceApi.DevicePropertyDetail[]) || []; // 后端直接返回数组,不是 { list: [] } 格式
list.value = (
Array.isArray(data) ? data : data?.list || []
) as IotDeviceApi.DevicePropertyDetail[];
total.value = list.value.length; total.value = list.value.length;
// 如果是图表模式且不是复杂数据类型,渲染图表 // 如果是图表模式且不是复杂数据类型,渲染图表
@@ -143,7 +148,9 @@ async function getList() {
!isComplexDataType.value && !isComplexDataType.value &&
list.value.length > 0 list.value.length > 0
) { ) {
// 等待 DOM 更新完成后再渲染图表
await nextTick(); await nextTick();
await nextTick(); // 双重 nextTick 确保 DOM 完全准备好
renderChart(); renderChart();
} }
} catch { } catch {
@@ -157,10 +164,19 @@ async function getList() {
/** 渲染图表 */ /** 渲染图表 */
function renderChart() { function renderChart() {
if (!list.value || list.value.length === 0) return; if (!list.value || list.value.length === 0) {
return;
}
const chartData = list.value.map((item) => [item.updateTime, item.value]); const chartData = list.value.map((item) => [item.updateTime, item.value]);
// 使用 setTimeout 延迟渲染,避免 ECharts 主进程冲突
setTimeout(() => {
// 检查 chartRef 是否存在且已挂载
if (!chartRef.value || !chartRef.value.$el) {
return;
}
renderEcharts({ renderEcharts({
title: { title: {
text: '属性值趋势', text: '属性值趋势',
@@ -265,6 +281,7 @@ function renderChart() {
}, },
], ],
}); });
}, 300); // 延迟300ms渲染确保 DOM 完全准备好
} }
/** 打开弹窗 */ /** 打开弹窗 */
@@ -275,12 +292,31 @@ async function open(deviceId: number, identifier: string, dataType: string) {
propertyIdentifier.value = identifier; propertyIdentifier.value = identifier;
thingModelDataType.value = dataType; thingModelDataType.value = dataType;
// 重置时间范围为最近7天
dateRange.value = [
dayjs().subtract(7, 'day').startOf('day'),
dayjs().endOf('day'),
];
// 更新查询参数的时间
queryParams.times = [
formatDateTime(dateRange.value[0].toDate()),
formatDateTime(dateRange.value[1].toDate()),
];
// 如果物模型是 struct、array需要默认使用 list 模式 // 如果物模型是 struct、array需要默认使用 list 模式
viewMode.value = isComplexDataType.value ? 'list' : 'chart'; viewMode.value = isComplexDataType.value ? 'list' : 'chart';
// 等待弹窗完全渲染后再获取数据 // 等待弹窗完全渲染后再获取数据
await nextTick(); await nextTick();
await getList(); await getList();
// 如果是图表模式,延迟渲染图表
if (viewMode.value === 'chart' && !isComplexDataType.value) {
setTimeout(() => {
renderChart();
}, 500);
}
} }
/** 时间变化处理 */ /** 时间变化处理 */
@@ -290,8 +326,8 @@ function handleTimeChange() {
} }
queryParams.times = [ queryParams.times = [
formatDate(dateRange.value[0].toDate()), formatDateTime(dateRange.value[0].toDate()),
formatDate(dateRange.value[1].toDate()), formatDateTime(dateRange.value[1].toDate()),
]; ];
getList(); getList();
@@ -372,8 +408,14 @@ watch(viewMode, async (newMode) => {
!isComplexDataType.value && !isComplexDataType.value &&
list.value.length > 0 list.value.length > 0
) { ) {
// 等待 DOM 显示完成
await nextTick(); await nextTick();
await nextTick();
// 延迟渲染图表
setTimeout(() => {
renderChart(); renderChart();
}, 300);
} }
}); });
@@ -398,7 +440,7 @@ defineExpose({ open }); // 提供 open 方法,用于打开弹窗
format="YYYY-MM-DD HH:mm:ss" format="YYYY-MM-DD HH:mm:ss"
:placeholder="['开始时间', '结束时间']" :placeholder="['开始时间', '结束时间']"
class="!w-[400px]" class="!w-[400px]"
@press-enter="handleTimeChange" @change="handleTimeChange"
/> />
<!-- 刷新按钮 --> <!-- 刷新按钮 -->
@@ -460,18 +502,20 @@ defineExpose({ open }); // 提供 open 方法,用于打开弹窗
<!-- 数据展示区域 --> <!-- 数据展示区域 -->
<Spin :spinning="loading" :delay="200"> <Spin :spinning="loading" :delay="200">
<!-- 图表模式 --> <!-- 图表模式 -->
<div v-if="viewMode === 'chart'" class="chart-container"> <div v-show="viewMode === 'chart'" class="chart-container">
<Empty <Empty
v-if="list.length === 0" v-if="list.length === 0"
:image="Empty.PRESENTED_IMAGE_SIMPLE" :image="Empty.PRESENTED_IMAGE_SIMPLE"
description="暂无数据" description="暂无数据"
class="py-20" class="py-20"
/> />
<EchartsUI v-else ref="chartRef" height="500px" /> <div v-else>
<EchartsUI ref="chartRef" height="500px" />
</div>
</div> </div>
<!-- 表格模式 --> <!-- 表格模式 -->
<div v-else class="table-container"> <div v-show="viewMode === 'list'" class="table-container">
<Table <Table
:data-source="list" :data-source="list"
:columns="tableColumns" :columns="tableColumns"

View File

@@ -52,7 +52,7 @@ const serviceThingModels = computed(() => {
}); });
/** 查询列表 */ /** 查询列表 */
const getList = async () => { async function getList() {
if (!props.deviceId) return; if (!props.deviceId) return;
loading.value = true; loading.value = true;
try { try {
@@ -62,43 +62,43 @@ const getList = async () => {
} finally { } finally {
loading.value = false; loading.value = false;
} }
}; }
/** 搜索按钮操作 */ /** 搜索按钮操作 */
const handleQuery = () => { function handleQuery() {
queryParams.pageNo = 1; queryParams.pageNo = 1;
getList(); getList();
}; }
/** 重置按钮操作 */ /** 重置按钮操作 */
const resetQuery = () => { function resetQuery() {
queryFormRef.value?.resetFields(); queryFormRef.value?.resetFields();
queryParams.identifier = ''; queryParams.identifier = '';
queryParams.times = []; queryParams.times = [];
handleQuery(); handleQuery();
}; }
/** 获取服务名称 */ /** 获取服务名称 */
const getServiceName = (identifier: string | undefined) => { function getServiceName(identifier: string | undefined) {
if (!identifier) return '-'; if (!identifier) return '-';
const service = serviceThingModels.value.find( const service = serviceThingModels.value.find(
(item: ThingModelData) => item.identifier === identifier, (item: ThingModelData) => item.identifier === identifier,
); );
return service?.name || identifier; return service?.name || identifier;
}; }
/** 获取调用方式 */ /** 获取调用方式 */
const getCallType = (identifier: string | undefined) => { function getCallType(identifier: string | undefined) {
if (!identifier) return '-'; if (!identifier) return '-';
const service = serviceThingModels.value.find( const service = serviceThingModels.value.find(
(item: ThingModelData) => item.identifier === identifier, (item: ThingModelData) => item.identifier === identifier,
); );
if (!service?.service?.callType) return '-'; if (!service?.service?.callType) return '-';
return getThingModelServiceCallTypeLabel(service.service.callType) || '-'; return getThingModelServiceCallTypeLabel(service.service.callType) || '-';
}; }
/** 解析参数 */ /** 解析参数 */
const parseParams = (params: string) => { function parseParams(params: string) {
if (!params) return '-'; if (!params) return '-';
try { try {
const parsed = JSON.parse(params); const parsed = JSON.parse(params);
@@ -109,7 +109,7 @@ const parseParams = (params: string) => {
} catch { } catch {
return params; return params;
} }
}; }
/** 初始化 */ /** 初始化 */
onMounted(() => { onMounted(() => {
@@ -148,6 +148,7 @@ onMounted(() => {
v-model:value="queryParams.times" v-model:value="queryParams.times"
show-time show-time
format="YYYY-MM-DD HH:mm:ss" format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 360px" style="width: 360px"
/> />
</Form.Item> </Form.Item>

View File

@@ -1,13 +1,12 @@
<script lang="ts" setup> <script setup lang="ts">
import type { IotDeviceApi } from '#/api/iot/device/device'; import type { IotDeviceApi } from '#/api/iot/device/device';
import type { IotProductApi } from '#/api/iot/product/product'; import type { IotProductApi } from '#/api/iot/product/product';
import type { ThingModelData } from '#/api/iot/thingmodel'; import type { ThingModelData } from '#/api/iot/thingmodel';
import { onMounted, ref, unref } from 'vue'; import { onMounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { Page } from '@vben/common-ui'; import { Page } from '@vben/common-ui';
import { useTabbarStore } from '@vben/stores';
import { message, Tabs } from 'ant-design-vue'; import { message, Tabs } from 'ant-design-vue';
@@ -20,33 +19,42 @@ import DeviceDetailsHeader from './DeviceDetailsHeader.vue';
import DeviceDetailsInfo from './DeviceDetailsInfo.vue'; import DeviceDetailsInfo from './DeviceDetailsInfo.vue';
import DeviceDetailsMessage from './DeviceDetailsMessage.vue'; import DeviceDetailsMessage from './DeviceDetailsMessage.vue';
import DeviceDetailsSimulator from './DeviceDetailsSimulator.vue'; import DeviceDetailsSimulator from './DeviceDetailsSimulator.vue';
import DeviceDetailsSubDevice from './DeviceDetailsSubDevice.vue';
import DeviceDetailsThingModel from './DeviceDetailsThingModel.vue'; import DeviceDetailsThingModel from './DeviceDetailsThingModel.vue';
defineOptions({ name: 'IoTDeviceDetail' }); defineOptions({ name: 'IoTDeviceDetail' });
const route = useRoute(); const route = useRoute();
const id = Number(route.params.id); // 将字符串转换为数字 const router = useRouter();
const loading = ref(true); // 加载中
const product = ref<IotProductApi.Product>({} as IotProductApi.Product); // 产品详情 const id = Number(route.params.id);
const device = ref<IotDeviceApi.Device>({} as IotDeviceApi.Device); // 设备详情 const loading = ref(true);
const activeTab = ref('info'); // 默认激活的标签页 const product = ref<IotProductApi.Product>({} as IotProductApi.Product);
const thingModelList = ref<ThingModelData[]>([]); // 物模型列表数据 const device = ref<IotDeviceApi.Device>({} as IotDeviceApi.Device);
const activeTab = ref('info');
const thingModelList = ref<ThingModelData[]>([]);
/** 获取设备详情 */ /** 获取设备详情 */
async function getDeviceData() { async function getDeviceData(deviceId: number) {
loading.value = true; loading.value = true;
try { try {
device.value = await getDevice(id); device.value = await getDevice(deviceId);
await getProductData(device.value.productId); await getProductData(device.value.productId);
await getThingModelList(device.value.productId); await getThingModelList(device.value.productId);
} catch {
message.error('获取设备详情失败');
} finally { } finally {
loading.value = false; loading.value = false;
} }
} }
/** 获取产品详情 */ /** 获取产品详情 */
async function getProductData(id: number) { async function getProductData(productId: number) {
product.value = await getProduct(id); try {
product.value = await getProduct(productId);
} catch (error) {
console.error('获取产品详情失败:', error);
}
} }
/** 获取物模型列表 */ /** 获取物模型列表 */
@@ -61,17 +69,20 @@ async function getThingModelList(productId: number) {
} }
/** 初始化 */ /** 初始化 */
const tabbarStore = useTabbarStore(); // 视图操作
const router = useRouter(); // 路由
const { currentRoute } = router;
onMounted(async () => { onMounted(async () => {
if (!id) { if (!id) {
message.warning({ content: '参数错误,产品不能为空!' }); message.warning('参数错误,设备不能为空!');
await tabbarStore.closeTab(unref(currentRoute), router); router.back();
return; return;
} }
await getDeviceData();
activeTab.value = (route.query.tab as string) || 'info'; await getDeviceData(id);
// 处理 tab 参数
const { tab } = route.query;
if (tab) {
activeTab.value = tab as string;
}
}); });
</script> </script>
<template> <template>
@@ -80,50 +91,55 @@ onMounted(async () => {
:loading="loading" :loading="loading"
:product="product" :product="product"
:device="device" :device="device"
@refresh="getDeviceData" @refresh="() => getDeviceData(id)"
/> />
<Tabs v-model:active-key="activeTab" class="device-detail-tabs mt-4"> <Tabs v-model:active-key="activeTab" class="mt-4">
<Tabs.Pane key="info" tab="设备信息"> <Tabs.TabPane key="info" tab="设备信息">
<DeviceDetailsInfo <DeviceDetailsInfo
v-if="activeTab === 'info'" v-if="activeTab === 'info'"
:product="product" :product="product"
:device="device" :device="device"
/> />
</Tabs.Pane> </Tabs.TabPane>
<Tabs.Pane key="model" tab="物模型数据"> <Tabs.TabPane key="model" tab="物模型数据">
<DeviceDetailsThingModel <DeviceDetailsThingModel
v-if="activeTab === 'model' && device.id" v-if="activeTab === 'model' && device.id"
:device-id="device.id" :device-id="device.id"
:thing-model-list="thingModelList" :thing-model-list="thingModelList"
/> />
</Tabs.Pane> </Tabs.TabPane>
<Tabs.Pane <Tabs.TabPane
v-if="product.deviceType === DeviceTypeEnum.GATEWAY" v-if="product.deviceType === DeviceTypeEnum.GATEWAY"
key="sub-device" key="sub-device"
tab="子设备管理" tab="子设备管理"
>
<DeviceDetailsSubDevice
v-if="activeTab === 'sub-device' && device.id"
:device-id="device.id"
/> />
<Tabs.Pane key="log" tab="设备消息"> </Tabs.TabPane>
<Tabs.TabPane key="log" tab="设备消息">
<DeviceDetailsMessage <DeviceDetailsMessage
v-if="activeTab === 'log' && device.id" v-if="activeTab === 'log' && device.id"
:device-id="device.id" :device-id="device.id"
/> />
</Tabs.Pane> </Tabs.TabPane>
<Tabs.Pane key="simulator" tab="模拟设备"> <Tabs.TabPane key="simulator" tab="模拟设备">
<DeviceDetailsSimulator <DeviceDetailsSimulator
v-if="activeTab === 'simulator'" v-if="activeTab === 'simulator'"
:product="product" :product="product"
:device="device" :device="device"
:thing-model-list="thingModelList" :thing-model-list="thingModelList"
/> />
</Tabs.Pane> </Tabs.TabPane>
<Tabs.Pane key="config" tab="设备配置"> <Tabs.TabPane key="config" tab="设备配置">
<DeviceDetailConfig <DeviceDetailConfig
v-if="activeTab === 'config'" v-if="activeTab === 'config'"
:device="device" :device="device"
@success="getDeviceData" @success="() => getDeviceData(id)"
/> />
</Tabs.Pane> </Tabs.TabPane>
</Tabs> </Tabs>
</Page> </Page>
</template> </template>

View File

@@ -34,11 +34,9 @@ export function useFormSchema(): VbenFormSchema[] {
label: '父级分组', label: '父级分组',
component: 'ApiTreeSelect', component: 'ApiTreeSelect',
componentProps: { componentProps: {
api: getSimpleDeviceGroupList, api: () => getSimpleDeviceGroupList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
placeholder: '请选择父级分组', placeholder: '请选择父级分组',
allowClear: true, allowClear: true,
}, },

View File

@@ -29,7 +29,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '所属产品', label: '所属产品',
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
api: getSimpleProductList, api: () => getSimpleProductList(),
labelField: 'name', labelField: 'name',
valueField: 'id', valueField: 'id',
placeholder: '请选择产品', placeholder: '请选择产品',
@@ -85,7 +85,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
label: '产品', label: '产品',
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
api: getSimpleProductList, api: () => getSimpleProductList(),
labelField: 'name', labelField: 'name',
valueField: 'id', valueField: 'id',
placeholder: '请选择产品', placeholder: '请选择产品',

View File

@@ -29,7 +29,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '所属产品', label: '所属产品',
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
api: getSimpleProductList, api: () => getSimpleProductList(),
labelField: 'name', labelField: 'name',
valueField: 'id', valueField: 'id',
placeholder: '请选择产品', placeholder: '请选择产品',
@@ -86,7 +86,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
label: '产品', label: '产品',
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
api: getSimpleProductList, api: () => getSimpleProductList(),
labelField: 'name', labelField: 'name',
valueField: 'id', valueField: 'id',
placeholder: '请选择产品', placeholder: '请选择产品',

View File

@@ -34,11 +34,9 @@ export function useFormSchema(): VbenFormSchema[] {
label: '父级分类', label: '父级分类',
component: 'ApiTreeSelect', component: 'ApiTreeSelect',
componentProps: { componentProps: {
api: getSimpleProductCategoryList, api: () => getSimpleProductCategoryList(),
fieldNames: { labelField: 'name',
label: 'name', valueField: 'id',
value: 'id',
},
placeholder: '请选择父级分类', placeholder: '请选择父级分类',
allowClear: true, allowClear: true,
}, },

View File

@@ -93,7 +93,7 @@ export function useFormSchema(formApi?: any): VbenFormSchema[] {
label: '产品分类', label: '产品分类',
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
api: getSimpleProductCategoryList, api: () => getSimpleProductCategoryList(),
labelField: 'name', labelField: 'name',
valueField: 'id', valueField: 'id',
placeholder: '请选择产品分类', placeholder: '请选择产品分类',
@@ -246,7 +246,7 @@ export function useBasicFormSchema(formApi?: any): VbenFormSchema[] {
label: '产品分类', label: '产品分类',
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
api: getSimpleProductCategoryList, api: () => getSimpleProductCategoryList(),
labelField: 'name', labelField: 'name',
valueField: 'id', valueField: 'id',
placeholder: '请选择产品分类', placeholder: '请选择产品分类',

View File

@@ -11,16 +11,15 @@ import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, Card, Image, Input, message, Space } from 'ant-design-vue'; import { Button, Card, Image, Input, message, Space } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getSimpleProductCategoryList } from '#/api/iot/product/category';
import { import {
deleteProduct, deleteProduct,
exportProduct, exportProduct,
getProductPage, getProductPage,
} from '#/api/iot/product/product'; } from '#/api/iot/product/product';
import { getSimpleProductCategoryList } from '#/api/iot/product/category';
import { $t } from '#/locales'; import { $t } from '#/locales';
import { useGridColumns, useImagePreview } from './data'; import { useGridColumns, useImagePreview } from './data';
// @ts-ignore
import ProductCardView from './modules/ProductCardView.vue'; import ProductCardView from './modules/ProductCardView.vue';
import ProductForm from './modules/ProductForm.vue'; import ProductForm from './modules/ProductForm.vue';

View File

@@ -237,12 +237,10 @@ defineExpose({
.product-card { .product-card {
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
border: 1px solid #e8e8e8;
border-radius: 8px; border-radius: 8px;
transition: all 0.3s ease; transition: all 0.3s ease;
&:hover { &:hover {
border-color: #d9d9d9;
box-shadow: 0 4px 16px rgb(0 0 0 / 8%); box-shadow: 0 4px 16px rgb(0 0 0 / 8%);
transform: translateY(-2px); transform: translateY(-2px);
} }
@@ -273,7 +271,6 @@ defineExpose({
font-size: 16px; font-size: 16px;
font-weight: 600; font-weight: 600;
line-height: 1.5; line-height: 1.5;
color: #1f2937;
white-space: nowrap; white-space: nowrap;
} }
@@ -292,14 +289,13 @@ defineExpose({
.info-label { .info-label {
flex-shrink: 0; flex-shrink: 0;
margin-right: 8px; margin-right: 8px;
color: #6b7280; opacity: 0.65;
} }
.info-value { .info-value {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
font-weight: 500; font-weight: 500;
color: #1f2937;
white-space: nowrap; white-space: nowrap;
&.text-primary { &.text-primary {
@@ -315,9 +311,9 @@ defineExpose({
font-family: 'Courier New', monospace; font-family: 'Courier New', monospace;
font-size: 12px; font-size: 12px;
vertical-align: middle; vertical-align: middle;
color: #374151;
white-space: nowrap; white-space: nowrap;
cursor: help; cursor: help;
opacity: 0.85;
} }
.info-tag { .info-tag {
@@ -337,6 +333,7 @@ defineExpose({
color: #667eea; color: #667eea;
background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%); background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%);
border-radius: 8px; border-radius: 8px;
opacity: 0.8;
} }
// 按钮组 // 按钮组
@@ -345,7 +342,7 @@ defineExpose({
gap: 8px; gap: 8px;
padding-top: 12px; padding-top: 12px;
margin-top: auto; margin-top: auto;
border-top: 1px solid #f0f0f0; border-top: 1px solid var(--ant-color-split);
.action-btn { .action-btn {
flex: 1; flex: 1;
@@ -392,4 +389,38 @@ defineExpose({
} }
} }
} }
// 夜间模式适配
html.dark {
.product-card-view {
.product-card {
&:hover {
box-shadow: 0 4px 16px rgb(0 0 0 / 30%);
}
.product-title {
color: rgb(255 255 255 / 85%);
}
.info-list {
.info-label {
color: rgb(255 255 255 / 65%);
}
.info-value {
color: rgb(255 255 255 / 85%);
}
.product-key {
color: rgb(255 255 255 / 75%);
}
}
.product-3d-icon {
color: #8b9cff;
background: linear-gradient(135deg, #667eea25 0%, #764ba225 100%);
}
}
}
}
</style> </style>

View File

@@ -14,13 +14,18 @@ import {
} from '#/api/iot/product/product'; } from '#/api/iot/product/product';
import { $t } from '#/locales'; import { $t } from '#/locales';
import { generateProductKey, useBasicFormSchema, useAdvancedFormSchema } from '../data'; import {
generateProductKey,
useAdvancedFormSchema,
useBasicFormSchema,
} from '../data';
defineOptions({ name: 'IoTProductForm' }); defineOptions({ name: 'IoTProductForm' });
const emit = defineEmits(['success']);
const CollapsePanel = Collapse.Panel; const CollapsePanel = Collapse.Panel;
const emit = defineEmits(['success']);
const formData = ref<any>(); const formData = ref<any>();
const getTitle = computed(() => { const getTitle = computed(() => {
return formData.value?.id ? '编辑产品' : '新增产品'; return formData.value?.id ? '编辑产品' : '新增产品';
@@ -84,7 +89,10 @@ const [Modal, modalApi] = useVbenModal({
}; };
} }
const values = { ...basicValues, ...advancedValues } as IotProductApi.Product; const values = {
...basicValues,
...advancedValues,
} as IotProductApi.Product;
const data = formData.value?.id const data = formData.value?.id
? { ...values, id: formData.value.id } ? { ...values, id: formData.value.id }
: values; : values;
@@ -132,7 +140,11 @@ const [Modal, modalApi] = useVbenModal({
}); });
// 如果有图标、图片或描述,自动展开折叠面板以便显示 // 如果有图标、图片或描述,自动展开折叠面板以便显示
if (formData.value.icon || formData.value.picUrl || formData.value.description) { if (
formData.value.icon ||
formData.value.picUrl ||
formData.value.description
) {
activeKey.value = ['advanced']; activeKey.value = ['advanced'];
} }
} catch (error) { } catch (error) {
@@ -151,9 +163,6 @@ const [Modal, modalApi] = useVbenModal({
<Form /> <Form />
<Collapse v-model:active-key="activeKey" class="mt-4"> <Collapse v-model:active-key="activeKey" class="mt-4">
<CollapsePanel key="advanced" header="更多设置"> <CollapsePanel key="advanced" header="更多设置">
<template #extra>
<span class="text-gray-500">📷</span>
</template>
<AdvancedForm /> <AdvancedForm />
</CollapsePanel> </CollapsePanel>
</Collapse> </Collapse>

View File

@@ -15,15 +15,15 @@ interface Props {
defineProps<Props>(); defineProps<Props>();
/** 格式化日期 */ /** 格式化日期 */
const formatDate = (date?: Date | string) => { function formatDate(date?: Date | string) {
if (!date) return '-'; if (!date) return '-';
return new Date(date).toLocaleString('zh-CN'); return new Date(date).toLocaleString('zh-CN');
}; }
</script> </script>
<template> <template>
<Card title="产品信息"> <Card title="产品信息">
<Descriptions bordered :column="3"> <Descriptions bordered :column="3" size="small">
<Descriptions.Item label="产品名称"> <Descriptions.Item label="产品名称">
{{ product.name }} {{ product.name }}
</Descriptions.Item> </Descriptions.Item>

View File

@@ -24,7 +24,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
label: '产品', label: '产品',
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
api: getSimpleProductList, api: () => getSimpleProductList(),
labelField: 'name', labelField: 'name',
valueField: 'id', valueField: 'id',
placeholder: '请选择产品', placeholder: '请选择产品',

View File

@@ -210,7 +210,8 @@ function handleOperatorChange() {
<!-- 设备状态条件配置 --> <!-- 设备状态条件配置 -->
<div <div
v-if=" v-if="
condition.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS condition.type ===
IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS.toString()
" "
class="gap-16px flex flex-col" class="gap-16px flex flex-col"
> >
@@ -222,7 +223,7 @@ function handleOperatorChange() {
<Select <Select
:model-value="condition.operator" :model-value="condition.operator"
@update:model-value=" @update:model-value="
(value) => updateConditionField('operator', value) (value: any) => updateConditionField('operator', value)
" "
placeholder="请选择操作符" placeholder="请选择操作符"
class="w-full" class="w-full"

View File

@@ -120,7 +120,7 @@ const timeValue2 = computed(() => {
* @param value 字段值 * @param value 字段值
*/ */
function updateConditionField(field: any, value: any) { function updateConditionField(field: any, value: any) {
condition.value[field] = value; (condition.value as any)[field] = value;
} }
/** /**

View File

@@ -33,7 +33,7 @@ const subGroup = useVModel(props, 'modelValue', emit);
const maxConditions = computed(() => props.maxConditions || 3); // 最大条件数量 const maxConditions = computed(() => props.maxConditions || 3); // 最大条件数量
/** 添加条件 */ /** 添加条件 */
const addCondition = async () => { async function addCondition() {
// 确保 subGroup.value 是一个数组 // 确保 subGroup.value 是一个数组
if (!subGroup.value) { if (!subGroup.value) {
subGroup.value = []; subGroup.value = [];
@@ -58,7 +58,7 @@ const addCondition = async () => {
if (subGroup.value) { if (subGroup.value) {
subGroup.value.push(newCondition); subGroup.value.push(newCondition);
} }
}; }
/** /**
* 移除条件 * 移除条件

View File

@@ -27,7 +27,7 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
{ {
field: 'type', field: 'type',
title: '功能类型', title: '功能类型',
minWidth: 100, minWidth: 20,
cellRender: { cellRender: {
name: 'CellDict', name: 'CellDict',
props: { type: DICT_TYPE.IOT_THING_MODEL_TYPE }, props: { type: DICT_TYPE.IOT_THING_MODEL_TYPE },
@@ -41,17 +41,17 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
{ {
field: 'identifier', field: 'identifier',
title: '标识符', title: '标识符',
minWidth: 150, minWidth: 20,
}, },
{ {
field: 'dataType', field: 'dataType',
title: '数据类型', title: '数据类型',
minWidth: 120, minWidth: 50,
slots: { default: 'dataType' }, slots: { default: 'dataType' },
}, },
{ {
field: 'dataDefinition', field: 'property',
title: '数据定义', title: '属性',
minWidth: 200, minWidth: 200,
slots: { default: 'dataDefinition' }, slots: { default: 'dataDefinition' },
}, },

View File

@@ -1,17 +1,21 @@
<script setup lang="ts"> <script setup lang="ts">
import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { IotProductApi } from '#/api/iot/product/product';
import type { ThingModelApi } from '#/api/iot/thingmodel';
import { onMounted, provide, ref } from 'vue';
import { Page } from '@vben/common-ui'; import { Page } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
import { Button, message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table'; import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getProduct } from '#/api/iot/product/product';
import { deleteThingModel, getThingModelPage } from '#/api/iot/thingmodel'; import { deleteThingModel, getThingModelPage } from '#/api/iot/thingmodel';
import { getDataTypeOptionsLabel } from '../utils/constants'; import { getDataTypeOptionsLabel, IOT_PROVIDE_KEY } from '../utils/constants';
import { useGridColumns, useGridFormSchema } from './data'; import { useGridColumns, useGridFormSchema } from './data';
import { DataDefinition } from './modules/components';
import ThingModelForm from './modules/ThingModelForm.vue';
import ThingModelTSL from './modules/ThingModelTSL.vue';
defineOptions({ name: 'IoTThingModel' }); defineOptions({ name: 'IoTThingModel' });
@@ -19,6 +23,16 @@ const props = defineProps<{
productId: number; productId: number;
}>(); }>();
// 产品信息
const product = ref<IotProductApi.Product>({} as IotProductApi.Product);
// 提供产品信息给子组件
provide(IOT_PROVIDE_KEY.PRODUCT, product);
// 组件引用
const thingModelFormRef = ref();
const thingModelTSLRef = ref();
const [Grid, gridApi] = useVbenVxeGrid({ const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: { gridOptions: {
columns: useGridColumns(), columns: useGridColumns(),
@@ -26,12 +40,12 @@ const [Grid, gridApi] = useVbenVxeGrid({
keepSource: true, keepSource: true,
proxyConfig: { proxyConfig: {
ajax: { ajax: {
query: async ({ page, form }) => { query: async ({ page }: any, formValues: any) => {
return await getThingModelPage({ return await getThingModelPage({
pageNo: page.currentPage, pageNo: page.currentPage,
pageSize: page.pageSize, pageSize: page.pageSize,
productId: props.productId, productId: props.productId,
...form, ...formValues,
}); });
}, },
}, },
@@ -44,26 +58,24 @@ const [Grid, gridApi] = useVbenVxeGrid({
refresh: true, refresh: true,
search: true, search: true,
}, },
} as VxeTableGridOptions<ThingModelApi.ThingModel>, },
formOptions: { formOptions: {
schema: useGridFormSchema(), schema: useGridFormSchema(),
}, },
}); });
// 新增功能 // 新增功能
const handleCreate = () => { function handleCreate() {
// TODO: 打开物模型表单 thingModelFormRef.value?.open('create');
console.error('新增功能'); }
};
// 编辑功能 // 编辑功能
const handleEdit = (row: any) => { function handleEdit(row: any) {
// TODO: 打开物模型表单 thingModelFormRef.value?.open('update', row.id);
console.error('编辑功能:', row); }
};
// 删除功能 // 删除功能
const handleDelete = async (row: any) => { async function handleDelete(row: any) {
try { try {
await deleteThingModel(row.id); await deleteThingModel(row.id);
message.success('删除成功'); message.success('删除成功');
@@ -71,32 +83,62 @@ const handleDelete = async (row: any) => {
} catch (error) { } catch (error) {
console.error('删除失败:', error); console.error('删除失败:', error);
} }
}; }
// 打开 TSL // 打开 TSL
const handleOpenTSL = () => { function handleOpenTSL() {
// TODO: 打开 TSL 弹窗 thingModelTSLRef.value?.open();
console.error('打开 TSL'); }
};
// 获取数据类型标签 // 获取数据类型标签
const getDataTypeLabel = (row: any) => { function getDataTypeLabel(row: any) {
return getDataTypeOptionsLabel(row.property?.dataType) || '-'; return getDataTypeOptionsLabel(row.property?.dataType) || '-';
}; }
// 刷新表格
function handleRefresh() {
gridApi.reload();
}
// 获取产品信息
async function getProductData() {
try {
product.value = await getProduct(props.productId);
} catch (error) {
console.error('获取产品信息失败:', error);
}
}
// 初始化
onMounted(async () => {
await getProductData();
});
</script> </script>
<template> <template>
<Page <Page
auto-content-height
description="管理产品的物模型定义,包括属性、服务和事件" description="管理产品的物模型定义,包括属性、服务和事件"
title="物模型管理" title="物模型管理"
> >
<Grid> <Grid>
<template #toolbar-tools> <template #toolbar-tools>
<Button @click="handleCreate"> <TableAction
<IconifyIcon icon="ant-design:plus-outlined" class="mr-1" /> :actions="[
添加功能 {
</Button> label: '添加功能',
<Button type="primary" @click="handleOpenTSL"> TSL </Button> type: 'primary',
icon: ACTION_ICON.ADD,
onClick: handleCreate,
},
{
label: 'TSL',
type: 'default',
color: 'success',
onClick: handleOpenTSL,
},
]"
/>
</template> </template>
<!-- 数据类型列 --> <!-- 数据类型列 -->
@@ -106,17 +148,38 @@ const getDataTypeLabel = (row: any) => {
<!-- 数据定义列 --> <!-- 数据定义列 -->
<template #dataDefinition="{ row }"> <template #dataDefinition="{ row }">
<!-- TODO: 实现数据定义组件 --> <DataDefinition :data="row" />
<span class="text-gray-400">{{ row }}</span>
</template> </template>
<!-- 操作列 --> <!-- 操作列 -->
<template #actions="{ row }"> <template #actions="{ row }">
<Button size="small" type="primary" @click="handleEdit(row)"> <TableAction
编辑 :actions="[
</Button> {
<Button size="small" danger @click="handleDelete(row)"> 删除 </Button> label: '编辑',
type: 'link',
icon: ACTION_ICON.EDIT,
onClick: handleEdit.bind(null, row),
},
{
label: '删除',
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
popConfirm: {
title: '确认删除该功能吗?',
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template> </template>
</Grid> </Grid>
<!-- 物模型表单 -->
<ThingModelForm ref="thingModelFormRef" @success="handleRefresh" />
<!-- TSL 弹窗 -->
<ThingModelTSL ref="thingModelTSLRef" />
</Page> </Page>
</template> </template>

View File

@@ -2,8 +2,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { Ref } from 'vue'; import type { Ref } from 'vue';
import type { ThingModelEvent } from '#/api/iot/thingmodel';
import { watch } from 'vue'; import { watch } from 'vue';
import { isEmpty } from '@vben/utils'; import { isEmpty } from '@vben/utils';
@@ -23,11 +21,7 @@ defineOptions({ name: 'ThingModelEvent' });
const props = defineProps<{ isStructDataSpecs?: boolean; modelValue: any }>(); const props = defineProps<{ isStructDataSpecs?: boolean; modelValue: any }>();
const emits = defineEmits(['update:modelValue']); const emits = defineEmits(['update:modelValue']);
const thingModelEvent = useVModel( const thingModelEvent = useVModel(props, 'modelValue', emits) as Ref<any>;
props,
'modelValue',
emits,
) as Ref<ThingModelEvent>;
// 默认选中INFO 信息 // 默认选中INFO 信息
watch( watch(
@@ -43,9 +37,9 @@ watch(
<Form.Item <Form.Item
:rules="[{ required: true, message: '请选择事件类型', trigger: 'change' }]" :rules="[{ required: true, message: '请选择事件类型', trigger: 'change' }]"
label="事件类型" label="事件类型"
prop="event.type" name="event.type"
> >
<Radio.Group v-model="thingModelEvent.type"> <Radio.Group v-model:value="thingModelEvent.type">
<Radio <Radio
v-for="eventType in Object.values(IoTThingModelEventTypeEnum)" v-for="eventType in Object.values(IoTThingModelEventTypeEnum)"
:key="eventType.value" :key="eventType.value"
@@ -57,7 +51,7 @@ watch(
</Form.Item> </Form.Item>
<Form.Item label="输出参数"> <Form.Item label="输出参数">
<ThingModelInputOutputParam <ThingModelInputOutputParam
v-model="thingModelEvent.outputData" v-model="thingModelEvent.outputParams"
:direction="IoTThingModelParamDirectionEnum.OUTPUT" :direction="IoTThingModelParamDirectionEnum.OUTPUT"
/> />
</Form.Item> </Form.Item>

View File

@@ -12,7 +12,7 @@ import { getDictOptions } from '@vben/hooks';
import { $t } from '@vben/locales'; import { $t } from '@vben/locales';
import { cloneDeep } from '@vben/utils'; import { cloneDeep } from '@vben/utils';
import { Button, Form, Input, message, Radio } from 'ant-design-vue'; import { Form, Input, message, Modal, Radio } from 'ant-design-vue';
import { import {
createThingModel, createThingModel,
@@ -40,8 +40,8 @@ const dialogVisible = ref(false); // 弹窗的是否展示
const dialogTitle = ref(''); // 弹窗的标题 const dialogTitle = ref(''); // 弹窗的标题
const formLoading = ref(false); // 表单的加载中1修改时的数据加载2提交的按钮禁用 const formLoading = ref(false); // 表单的加载中1修改时的数据加载2提交的按钮禁用
const formType = ref(''); // 表单的类型create - 新增update - 修改 const formType = ref(''); // 表单的类型create - 新增update - 修改
const formData = ref<ThingModelData>({ const formData = ref<any>({
type: IoTThingModelTypeEnum.PROPERTY.toString(), type: IoTThingModelTypeEnum.PROPERTY,
dataType: IoTDataSpecsDataTypeEnum.INT, dataType: IoTDataSpecsDataTypeEnum.INT,
property: { property: {
dataType: IoTDataSpecsDataTypeEnum.INT, dataType: IoTDataSpecsDataTypeEnum.INT,
@@ -49,22 +49,34 @@ const formData = ref<ThingModelData>({
dataType: IoTDataSpecsDataTypeEnum.INT, dataType: IoTDataSpecsDataTypeEnum.INT,
}, },
}, },
service: {}, service: {
event: {}, inputParams: [],
outputParams: [],
},
event: {
outputParams: [],
},
}); });
const formRef = ref(); // 表单 Ref const formRef = ref(); // 表单 Ref
/** 打开弹窗 */ /** 打开弹窗 */
const open = async (type: string, id?: number) => { async function open(type: string, id?: number) {
dialogVisible.value = true; dialogVisible.value = true;
dialogTitle.value = $t(`action.${type}`); // 设置标题create -> 新增update -> 编辑
dialogTitle.value =
type === 'create' ? $t('page.action.add') : $t('page.action.edit');
formType.value = type; formType.value = type;
resetForm(); resetForm();
if (id) { if (id) {
formLoading.value = true; formLoading.value = true;
try { try {
formData.value = await getThingModel(id); const result = await getThingModel(id);
// 转换类型为数字
formData.value = {
...result,
type: Number(result.type),
};
// 情况一:属性初始化 // 情况一:属性初始化
if ( if (
!formData.value.property || !formData.value.property ||
@@ -77,26 +89,56 @@ const open = async (type: string, id?: number) => {
dataType: IoTDataSpecsDataTypeEnum.INT, dataType: IoTDataSpecsDataTypeEnum.INT,
}, },
}; };
} else {
// 确保 dataSpecs 和 dataSpecsList 存在
if (!formData.value.property.dataSpecs) {
formData.value.property.dataSpecs = {};
}
if (!formData.value.property.dataSpecsList) {
formData.value.property.dataSpecsList = [];
}
// 如果 property.dataType 不存在,设置为默认值
if (!formData.value.property.dataType) {
formData.value.property.dataType = IoTDataSpecsDataTypeEnum.INT;
}
} }
// 情况二:服务初始化 // 情况二:服务初始化
if ( if (
!formData.value.service || !formData.value.service ||
Object.keys(formData.value.service).length === 0 Object.keys(formData.value.service).length === 0
) { ) {
formData.value.service = {}; formData.value.service = {
inputParams: [],
outputParams: [],
};
} else {
// 确保参数数组存在
if (!formData.value.service.inputParams) {
formData.value.service.inputParams = [];
}
if (!formData.value.service.outputParams) {
formData.value.service.outputParams = [];
}
} }
// 情况三:事件初始化 // 情况三:事件初始化
if ( if (
!formData.value.event || !formData.value.event ||
Object.keys(formData.value.event).length === 0 Object.keys(formData.value.event).length === 0
) { ) {
formData.value.event = {}; formData.value.event = {
outputParams: [],
};
} else {
// 确保参数数组存在
if (!formData.value.event.outputParams) {
formData.value.event.outputParams = [];
}
} }
} finally { } finally {
formLoading.value = false; formLoading.value = false;
} }
} }
}; }
defineExpose({ open, close: () => (dialogVisible.value = false) }); defineExpose({ open, close: () => (dialogVisible.value = false) });
async function submitForm() { async function submitForm() {
@@ -137,6 +179,13 @@ function fillExtraAttributes(data: any) {
data.dataType = data.service.dataType; data.dataType = data.service.dataType;
data.service.identifier = data.identifier; data.service.identifier = data.identifier;
data.service.name = data.name; data.service.name = data.name;
// 保留输入输出参数,但如果为空数组则删除
if (!data.service.inputParams || data.service.inputParams.length === 0) {
delete data.service.inputParams;
}
if (!data.service.outputParams || data.service.outputParams.length === 0) {
delete data.service.outputParams;
}
delete data.property; delete data.property;
delete data.event; delete data.event;
} }
@@ -146,6 +195,10 @@ function fillExtraAttributes(data: any) {
data.dataType = data.event.dataType; data.dataType = data.event.dataType;
data.event.identifier = data.identifier; data.event.identifier = data.identifier;
data.event.name = data.name; data.event.name = data.name;
// 保留输出参数,但如果为空数组则删除
if (!data.event.outputParams || data.event.outputParams.length === 0) {
delete data.event.outputParams;
}
delete data.property; delete data.property;
delete data.service; delete data.service;
} }
@@ -164,7 +217,7 @@ function removeDataSpecs(val: any) {
/** 重置表单 */ /** 重置表单 */
function resetForm() { function resetForm() {
formData.value = { formData.value = {
type: IoTThingModelTypeEnum.PROPERTY.toString(), type: IoTThingModelTypeEnum.PROPERTY,
dataType: IoTDataSpecsDataTypeEnum.INT, dataType: IoTDataSpecsDataTypeEnum.INT,
property: { property: {
dataType: IoTDataSpecsDataTypeEnum.INT, dataType: IoTDataSpecsDataTypeEnum.INT,
@@ -172,18 +225,27 @@ function resetForm() {
dataType: IoTDataSpecsDataTypeEnum.INT, dataType: IoTDataSpecsDataTypeEnum.INT,
}, },
}, },
service: {}, service: {
event: {}, inputParams: [],
outputParams: [],
},
event: {
outputParams: [],
},
}; };
formRef.value?.resetFields(); formRef.value?.resetFields();
} }
</script> </script>
<template> <template>
<Modal v-model="dialogVisible" :title="dialogTitle"> <Modal
v-model:open="dialogVisible"
:title="dialogTitle"
:confirm-loading="formLoading"
@ok="submitForm"
>
<Form <Form
ref="formRef" ref="formRef"
:loading="formLoading"
:model="formData" :model="formData"
:label-col="{ span: 6 }" :label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }" :wrapper-col="{ span: 18 }"
@@ -191,12 +253,9 @@ function resetForm() {
<Form.Item label="功能类型" name="type"> <Form.Item label="功能类型" name="type">
<Radio.Group v-model:value="formData.type"> <Radio.Group v-model:value="formData.type">
<Radio.Button <Radio.Button
v-for="(dict, index) in getDictOptions( v-for="dict in getDictOptions(DICT_TYPE.IOT_THING_MODEL_TYPE)"
DICT_TYPE.IOT_THING_MODEL_TYPE, :key="String(dict.value)"
'number', :value="Number(dict.value)"
)"
:key="index"
:value="dict.value"
> >
{{ dict.label }} {{ dict.label }}
</Radio.Button> </Radio.Button>
@@ -210,17 +269,17 @@ function resetForm() {
</Form.Item> </Form.Item>
<!-- 属性配置 --> <!-- 属性配置 -->
<ThingModelProperty <ThingModelProperty
v-if="formData.type === IoTThingModelTypeEnum.PROPERTY.toString()" v-if="formData.type === IoTThingModelTypeEnum.PROPERTY"
v-model="formData.property" v-model="formData.property"
/> />
<!-- 服务配置 --> <!-- 服务配置 -->
<ThingModelService <ThingModelService
v-if="formData.type === IoTThingModelTypeEnum.SERVICE.toString()" v-if="formData.type === IoTThingModelTypeEnum.SERVICE"
v-model="formData.service" v-model="formData.service"
/> />
<!-- 事件配置 --> <!-- 事件配置 -->
<ThingModelEvent <ThingModelEvent
v-if="formData.type === IoTThingModelTypeEnum.EVENT.toString()" v-if="formData.type === IoTThingModelTypeEnum.EVENT"
v-model="formData.event" v-model="formData.event"
/> />
<Form.Item label="描述" name="desc"> <Form.Item label="描述" name="desc">
@@ -232,12 +291,5 @@ function resetForm() {
/> />
</Form.Item> </Form.Item>
</Form> </Form>
<template #footer>
<Button :disabled="formLoading" type="primary" @click="submitForm">
确 定
</Button>
<Button @click="dialogVisible = false"> </Button>
</template>
</Modal> </Modal>
</template> </template>

View File

@@ -29,6 +29,7 @@ const formData = ref<any>({
dataSpecs: { dataSpecs: {
dataType: IoTDataSpecsDataTypeEnum.INT, dataType: IoTDataSpecsDataTypeEnum.INT,
}, },
dataSpecsList: [],
}, },
}); });
@@ -40,16 +41,22 @@ function openParamForm(val: any) {
return; return;
} }
// 编辑时回显数据 // 编辑时回显数据
const valData = val as any;
formData.value = { formData.value = {
identifier: val.identifier, identifier: valData?.identifier || '',
name: val.name, name: valData?.name || '',
description: val.description, description: valData?.description || '',
property: { property: {
dataType: val.dataType, dataType: valData?.dataType || IoTDataSpecsDataTypeEnum.INT,
dataSpecs: val.dataSpecs, dataSpecs: valData?.dataSpecs ?? {},
dataSpecsList: val.dataSpecsList, dataSpecsList: valData?.dataSpecsList ?? [],
}, },
}; };
// 确保 property.dataType 有值
if (!formData.value.property.dataType) {
formData.value.property.dataType = IoTDataSpecsDataTypeEnum.INT;
}
} }
/** 删除 param 项 */ /** 删除 param 项 */
@@ -108,6 +115,7 @@ function resetForm() {
dataSpecs: { dataSpecs: {
dataType: IoTDataSpecsDataTypeEnum.INT, dataType: IoTDataSpecsDataTypeEnum.INT,
}, },
dataSpecsList: [],
}, },
}; };
paramFormRef.value?.resetFields(); paramFormRef.value?.resetFields();
@@ -122,35 +130,34 @@ function resetForm() {
> >
<span>参数名称{{ item.name }}</span> <span>参数名称{{ item.name }}</span>
<div class="btn"> <div class="btn">
<Button link type="primary" @click="openParamForm(item)"> 编辑 </Button> <Button type="link" @click="openParamForm(item)">编辑</Button>
<Divider direction="vertical" /> <Divider type="vertical" />
<Button link danger @click="deleteParamItem(index)"> 删除 </Button> <Button type="link" danger @click="deleteParamItem(index)">删除</Button>
</div> </div>
</div> </div>
<Button link type="primary" @click="openParamForm(null)"> +新增参数 </Button> <Button type="link" @click="openParamForm(null)">+新增参数</Button>
<!-- param 表单 --> <!-- param 表单 -->
<Modal v-model="dialogVisible" title="新增参数" append-to-body> <Modal
v-model:open="dialogVisible"
title="新增参数"
:confirm-loading="formLoading"
@ok="submitForm"
>
<Form <Form
ref="paramFormRef" ref="paramFormRef"
v-loading="formLoading"
:model="formData" :model="formData"
label-width="100px" :label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
> >
<Form.Item label="参数名称" prop="name"> <Form.Item label="参数名称" name="name">
<Input v-model="formData.name" placeholder="请输入功能名称" /> <Input v-model:value="formData.name" placeholder="请输入功能名称" />
</Form.Item> </Form.Item>
<Form.Item label="标识符" prop="identifier"> <Form.Item label="标识符" name="identifier">
<Input v-model="formData.identifier" placeholder="请输入标识符" /> <Input v-model:value="formData.identifier" placeholder="请输入标识符" />
</Form.Item> </Form.Item>
<!-- 属性配置 --> <!-- 属性配置 -->
<ThingModelProperty v-model="formData.property" is-params /> <ThingModelProperty v-model="formData.property" is-params />
</Form> </Form>
<template #footer>
<Button :disabled="formLoading" type="primary" @click="submitForm">
</Button>
<Button @click="dialogVisible = false"> </Button>
</template>
</Modal> </Modal>
</template> </template>

View File

@@ -11,7 +11,6 @@ import { isEmpty } from '@vben/utils';
import { useVModel } from '@vueuse/core'; import { useVModel } from '@vueuse/core';
import { Form, Input, Radio, Select } from 'ant-design-vue'; import { Form, Input, Radio, Select } from 'ant-design-vue';
import { validateBoolName } from '#/api/iot/thingmodel';
import { import {
getDataTypeOptions, getDataTypeOptions,
IoTDataSpecsDataTypeEnum, IoTDataSpecsDataTypeEnum,
@@ -82,6 +81,7 @@ function handleChange(dataType: any) {
break; break;
} }
} }
// useVModel 会自动同步数据到父组件,不需要手动 emit
} }
/** 默认选中读写 */ /** 默认选中读写 */
@@ -91,22 +91,18 @@ watch(
if (props.isStructDataSpecs || props.isParams) { if (props.isStructDataSpecs || props.isParams) {
return; return;
} }
isEmpty(val) && if (isEmpty(val)) {
(property.value.accessMode = property.value.accessMode = IoTThingModelAccessModeEnum.READ_WRITE.value;
IoTThingModelAccessModeEnum.READ_WRITE.value); }
}, },
{ immediate: true }, { immediate: true },
); );
</script> </script>
<template> <template>
<Form.Item <Form.Item label="数据类型">
:rules="[{ required: true, message: '请选择数据类型', trigger: 'change' }]"
label="数据类型"
prop="property.dataType"
>
<Select <Select
v-model="property.dataType" v-model:value="property.dataType"
placeholder="请选择数据类型" placeholder="请选择数据类型"
@change="handleChange" @change="handleChange"
> >
@@ -114,9 +110,10 @@ watch(
<Select.Option <Select.Option
v-for="option in getDataTypeOptions2" v-for="option in getDataTypeOptions2"
:key="option.value" :key="option.value"
:label="`${option.value}(${option.label})`"
:value="option.value" :value="option.value"
/> >
{{ `${option.value}(${option.label})` }}
</Select.Option>
</Select> </Select>
</Form.Item> </Form.Item>
<!-- 数值型配置 --> <!-- 数值型配置 -->
@@ -140,24 +137,17 @@ watch(
v-if="property.dataType === IoTDataSpecsDataTypeEnum.BOOL" v-if="property.dataType === IoTDataSpecsDataTypeEnum.BOOL"
label="布尔值" label="布尔值"
> >
<template v-for="(item, index) in property.dataSpecsList" :key="item.value"> <template v-for="item in property.dataSpecsList" :key="item.value">
<div class="w-1/1 mb-5px flex items-center justify-start"> <div class="w-1/1 mb-5px flex items-center justify-start">
<span>{{ item.value }}</span> <span>{{ item.value }}</span>
<span class="mx-2">-</span> <span class="mx-2">-</span>
<Form.Item <div class="flex-1">
:prop="`property.dataSpecsList[${index}].name`"
:rules="[
{ required: true, message: '枚举描述不能为空' },
{ validator: validateBoolName, trigger: 'blur' },
]"
class="mb-0 flex-1"
>
<Input <Input
v-model="item.name" v-model:value="item.name"
:placeholder="`如:${item.value === 0 ? '关' : '开'}`" :placeholder="`如:${item.value === 0 ? '关' : '开'}`"
class="w-255px!" class="w-255px!"
/> />
</Form.Item> </div>
</div> </div>
</template> </template>
</Form.Item> </Form.Item>
@@ -165,21 +155,21 @@ watch(
<Form.Item <Form.Item
v-if="property.dataType === IoTDataSpecsDataTypeEnum.TEXT" v-if="property.dataType === IoTDataSpecsDataTypeEnum.TEXT"
label="数据长度" label="数据长度"
prop="property.dataSpecs.length" name="property.dataSpecs.length"
> >
<Input <Input
v-model="property.dataSpecs.length" v-model:value="property.dataSpecs.length"
class="w-255px!" class="w-255px!"
placeholder="请输入文本字节长度" placeholder="请输入文本字节长度"
> >
<template #append>字节</template> <template #addonAfter>字节</template>
</Input> </Input>
</Form.Item> </Form.Item>
<!-- 时间型配置 --> <!-- 时间型配置 -->
<Form.Item <Form.Item
v-if="property.dataType === IoTDataSpecsDataTypeEnum.DATE" v-if="property.dataType === IoTDataSpecsDataTypeEnum.DATE"
label="时间格式" label="时间格式"
prop="date" name="date"
> >
<Input <Input
class="w-255px!" class="w-255px!"
@@ -200,13 +190,13 @@ watch(
<Form.Item <Form.Item
v-if="!isStructDataSpecs && !isParams" v-if="!isStructDataSpecs && !isParams"
label="读写类型" label="读写类型"
prop="property.accessMode" name="property.accessMode"
> >
<Radio.Group v-model="property.accessMode"> <Radio.Group v-model:value="property.accessMode">
<Radio <Radio
v-for="accessMode in Object.values(IoTThingModelAccessModeEnum)" v-for="accessMode in Object.values(IoTThingModelAccessModeEnum)"
:key="accessMode.value" :key="accessMode.value"
:label="accessMode.value" :value="accessMode.value"
> >
{{ accessMode.label }} {{ accessMode.label }}
</Radio> </Radio>

View File

@@ -2,8 +2,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { Ref } from 'vue'; import type { Ref } from 'vue';
import type { ThingModelService } from '#/api/iot/thingmodel';
import { watch } from 'vue'; import { watch } from 'vue';
import { isEmpty } from '@vben/utils'; import { isEmpty } from '@vben/utils';
@@ -23,7 +21,7 @@ defineOptions({ name: 'ThingModelService' });
const props = defineProps<{ isStructDataSpecs?: boolean; modelValue: any }>(); const props = defineProps<{ isStructDataSpecs?: boolean; modelValue: any }>();
const emits = defineEmits(['update:modelValue']); const emits = defineEmits(['update:modelValue']);
const service = useVModel(props, 'modelValue', emits) as Ref<ThingModelService>; const service = useVModel(props, 'modelValue', emits) as Ref<any>;
/** 默认选中ASYNC 异步 */ /** 默认选中ASYNC 异步 */
watch( watch(
@@ -39,9 +37,9 @@ watch(
<Form.Item <Form.Item
:rules="[{ required: true, message: '请选择调用方式', trigger: 'change' }]" :rules="[{ required: true, message: '请选择调用方式', trigger: 'change' }]"
label="调用方式" label="调用方式"
prop="service.callType" name="service.callType"
> >
<Radio.Group v-model="service.callType"> <Radio.Group v-model:value="service.callType">
<Radio <Radio
v-for="callType in Object.values(IoTThingModelServiceCallTypeEnum)" v-for="callType in Object.values(IoTThingModelServiceCallTypeEnum)"
:key="callType.value" :key="callType.value"
@@ -53,13 +51,13 @@ watch(
</Form.Item> </Form.Item>
<Form.Item label="输入参数"> <Form.Item label="输入参数">
<ThingModelInputOutputParam <ThingModelInputOutputParam
v-model="service.inputData" v-model="service.inputParams"
:direction="IoTThingModelParamDirectionEnum.INPUT" :direction="IoTThingModelParamDirectionEnum.INPUT"
/> />
</Form.Item> </Form.Item>
<Form.Item label="输出参数"> <Form.Item label="输出参数">
<ThingModelInputOutputParam <ThingModelInputOutputParam
v-model="service.outputData" v-model="service.outputParams"
:direction="IoTThingModelParamDirectionEnum.OUTPUT" :direction="IoTThingModelParamDirectionEnum.OUTPUT"
/> />
</Form.Item> </Form.Item>

View File

@@ -3,58 +3,115 @@ import type { Ref } from 'vue';
import type { IotProductApi } from '#/api/iot/product/product'; import type { IotProductApi } from '#/api/iot/product/product';
import { inject, onMounted, ref } from 'vue'; import { computed, inject, ref, watch } from 'vue';
import { Modal, Radio } from 'ant-design-vue'; import { Modal, Radio, Textarea } from 'ant-design-vue';
import hljs from 'highlight.js'; // 导入代码高亮文件
import json from 'highlight.js/lib/languages/json';
import { getThingModelListByProductId } from '#/api/iot/thingmodel'; import { getThingModelTSL } from '#/api/iot/thingmodel';
import { IOT_PROVIDE_KEY } from '#/views/iot/utils/constants'; import { IOT_PROVIDE_KEY } from '#/views/iot/utils/constants';
import 'highlight.js/styles/github.css'; // 导入代码高亮样式
defineOptions({ name: 'ThingModelTSL' }); defineOptions({ name: 'ThingModelTSL' });
const dialogVisible = ref(false); // 弹窗的是否展示 const dialogVisible = ref(false); // 弹窗的是否展示
const dialogTitle = ref('物模型 TSL'); // 弹窗的标题 const dialogTitle = ref('物模型 TSL'); // 弹窗的标题
const product = inject<Ref<IotProductApi.Product>>(IOT_PROVIDE_KEY.PRODUCT); // 注入产品信息 const product = inject<Ref<IotProductApi.Product>>(IOT_PROVIDE_KEY.PRODUCT); // 注入产品信息
const viewMode = ref('code'); // 查看模式:code-代码视图editor-编辑器视图 const viewMode = ref('view'); // 查看模式:view-代码视图editor-编辑器视图
/** 打开弹窗 */ /** 打开弹窗 */
function open() { async function open() {
dialogVisible.value = true; dialogVisible.value = true;
await getTsl();
} }
defineExpose({ open }); defineExpose({ open });
/** 获取 TSL */ /** 获取 TSL */
const thingModelTSL = ref({}); const thingModelTSL = ref<any>({});
const tslString = ref(''); // 用于编辑器的字符串格式
async function getTsl() { async function getTsl() {
thingModelTSL.value = await getThingModelListByProductId( try {
product?.value?.id || 0, thingModelTSL.value = await getThingModelTSL(product?.value?.id || 0);
); // 将对象转换为格式化的 JSON 字符串
tslString.value = JSON.stringify(thingModelTSL.value, null, 2);
} catch (error) {
console.error('获取 TSL 失败:', error);
thingModelTSL.value = {};
tslString.value = '{}';
}
} }
/** 初始化 */ /** 格式化的 TSL 用于只读展示 */
onMounted(async () => { const formattedTSL = computed(() => {
// 注册代码高亮的各种语言 try {
hljs.registerLanguage('json', json); if (typeof thingModelTSL.value === 'string') {
await getTsl(); return JSON.stringify(JSON.parse(thingModelTSL.value), null, 2);
}
return JSON.stringify(thingModelTSL.value, null, 2);
} catch {
return JSON.stringify(thingModelTSL.value, null, 2);
}
});
/** 监听编辑器内容变化,实时更新数据 */
watch(tslString, (newValue) => {
try {
thingModelTSL.value = JSON.parse(newValue);
} catch {
// JSON 解析失败时保持原值
}
}); });
</script> </script>
<template> <template>
<Modal v-model="dialogVisible" :title="dialogTitle"> <Modal
<JsonEditor v-model:open="dialogVisible"
v-model="thingModelTSL" :title="dialogTitle"
:mode="viewMode === 'editor' ? 'code' : 'view'" :footer="null"
height="600px" width="800px"
/> >
<template #footer> <div class="mb-4">
<Radio.Group v-model="viewMode" size="small"> <Radio.Group v-model:value="viewMode" size="small">
<Radio.Button label="code">代码视图</Radio.Button> <Radio.Button value="view">代码视图</Radio.Button>
<Radio.Button label="editor">编辑器视图</Radio.Button> <Radio.Button value="editor">编辑器视图</Radio.Button>
</Radio.Group> </Radio.Group>
</template> </div>
<!-- 代码视图 - 只读展示 -->
<div v-if="viewMode === 'view'" class="json-viewer-container">
<pre class="json-code"><code>{{ formattedTSL }}</code></pre>
</div>
<!-- 编辑器视图 - 可编辑 -->
<Textarea
v-else
v-model:value="tslString"
:rows="20"
placeholder="请输入 JSON 格式的物模型 TSL"
class="json-editor"
/>
</Modal> </Modal>
</template> </template>
<style scoped>
.json-viewer-container {
max-height: 600px;
padding: 12px;
overflow-y: auto;
background-color: #f5f5f5;
border: 1px solid #d9d9d9;
border-radius: 4px;
}
.json-code {
margin: 0;
font-family: Monaco, Menlo, 'Ubuntu Mono', Consolas, monospace;
font-size: 13px;
line-height: 1.5;
color: #333;
word-wrap: break-word;
white-space: pre-wrap;
}
.json-editor {
font-family: Monaco, Menlo, 'Ubuntu Mono', Consolas, monospace;
font-size: 13px;
}
</style>

View File

@@ -1,6 +1,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { ThingModelData } from '#/api/iot/thingmodel'; import type { ThingModelData } from '#/api/iot/thingmodel';
import { computed } from 'vue';
import { Tooltip } from 'ant-design-vue';
import { import {
getEventTypeLabel, getEventTypeLabel,
getThingModelServiceCallTypeLabel, getThingModelServiceCallTypeLabel,
@@ -11,12 +15,40 @@ import {
/** 数据定义展示组件 */ /** 数据定义展示组件 */
defineOptions({ name: 'DataDefinition' }); defineOptions({ name: 'DataDefinition' });
defineProps<{ data: ThingModelData }>(); const props = defineProps<{ data: ThingModelData }>();
// 格式化布尔值和枚举值列表为字符串
const formattedDataSpecsList = computed(() => {
if (
!props.data.property?.dataSpecsList ||
props.data.property.dataSpecsList.length === 0
) {
return '';
}
return props.data.property.dataSpecsList
.map((item) => `${item.value}-${item.name}`)
.join('、');
});
// 显示的简短文本(第一个值)
const shortText = computed(() => {
if (
!props.data.property?.dataSpecsList ||
props.data.property.dataSpecsList.length === 0
) {
return '-';
}
const first = props.data.property.dataSpecsList[0];
const count = props.data.property.dataSpecsList.length;
return count > 1
? `${first.value}-${first.name}${count}`
: `${first.value}-${first.name}`;
});
</script> </script>
<template> <template>
<!-- 属性 --> <!-- 属性 -->
<template v-if="data.type === IoTThingModelTypeEnum.PROPERTY.toString()"> <template v-if="Number(data.type) === IoTThingModelTypeEnum.PROPERTY">
<!-- 非列表型数值 --> <!-- 非列表型数值 -->
<div <div
v-if=" v-if="
@@ -28,12 +60,12 @@ defineProps<{ data: ThingModelData }>();
" "
> >
取值范围:{{ 取值范围:{{
`${data.property?.dataSpecs.min}~${data.property?.dataSpecs.max}` `${data.property?.dataSpecs?.min}~${data.property?.dataSpecs?.max}`
}} }}
</div> </div>
<!-- 非列表型:文本 --> <!-- 非列表型:文本 -->
<div v-if="IoTDataSpecsDataTypeEnum.TEXT === data.property?.dataType"> <div v-if="IoTDataSpecsDataTypeEnum.TEXT === data.property?.dataType">
数据长度:{{ data.property?.dataSpecs.length }} 数据长度:{{ data.property?.dataSpecs?.length }}
</div> </div>
<!-- 列表型: 数组、结构、时间(特殊) --> <!-- 列表型: 数组、结构、时间(特殊) -->
<div <div
@@ -55,28 +87,37 @@ defineProps<{ data: ThingModelData }>();
) )
" "
> >
<div> <Tooltip :title="formattedDataSpecsList" placement="topLeft">
<span class="data-specs-text">
{{ {{
IoTDataSpecsDataTypeEnum.BOOL === data.property?.dataType IoTDataSpecsDataTypeEnum.BOOL === data.property?.dataType
? '布尔值' ? '布尔值'
: '枚举值' : '枚举值'
}} }}{{ shortText }}
</div> </span>
<div v-for="item in data.property?.dataSpecsList" :key="item.value"> </Tooltip>
{{ `${item.name}-${item.value}` }}
</div>
</div> </div>
</template> </template>
<!-- 服务 --> <!-- 服务 -->
<div v-if="data.type === IoTThingModelTypeEnum.SERVICE.toString()"> <div v-if="Number(data.type) === IoTThingModelTypeEnum.SERVICE">
调用方式:{{ 调用方式:{{
getThingModelServiceCallTypeLabel(data.service?.callType as any) getThingModelServiceCallTypeLabel(data.service?.callType as any)
}} }}
</div> </div>
<!-- 事件 --> <!-- 事件 -->
<div v-if="data.type === IoTThingModelTypeEnum.EVENT.toString()"> <div v-if="Number(data.type) === IoTThingModelTypeEnum.EVENT">
事件类型:{{ getEventTypeLabel(data.event?.type as any) }} 事件类型:{{ getEventTypeLabel(data.event?.type as any) }}
</div> </div>
</template> </template>
<style lang="scss" scoped></style> <style lang="scss" scoped>
.data-specs-text {
cursor: help;
border-bottom: 1px dashed #d9d9d9;
&:hover {
color: #1890ff;
border-bottom-color: #1890ff;
}
}
</style>

View File

@@ -29,8 +29,8 @@ function handleChange(val: any) {
</script> </script>
<template> <template>
<Form.Item label="元素类型" prop="property.dataSpecs.childDataType"> <Form.Item label="元素类型" name="property.dataSpecs.childDataType">
<Radio.Group v-model="dataSpecs.childDataType" @change="handleChange"> <Radio.Group v-model:value="dataSpecs.childDataType" @change="handleChange">
<template v-for="item in getDataTypeOptions()" :key="item.value"> <template v-for="item in getDataTypeOptions()" :key="item.value">
<Radio <Radio
v-if=" v-if="
@@ -50,8 +50,11 @@ function handleChange(val: any) {
</template> </template>
</Radio.Group> </Radio.Group>
</Form.Item> </Form.Item>
<Form.Item label="元素个数" prop="property.dataSpecs.size"> <Form.Item label="元素个数" name="property.dataSpecs.size">
<Input v-model="dataSpecs.size" placeholder="请输入数组中的元素个数" /> <Input
v-model:value="dataSpecs.size"
placeholder="请输入数组中的元素个数"
/>
</Form.Item> </Form.Item>
<!-- Struct 型配置--> <!-- Struct 型配置-->
<ThingModelStructDataSpecs <ThingModelStructDataSpecs

View File

@@ -2,31 +2,22 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { Ref } from 'vue'; import type { Ref } from 'vue';
import type { DataSpecsEnumOrBoolData } from '#/api/iot/thingmodel';
import { isEmpty } from '@vben/utils';
import { useVModel } from '@vueuse/core'; import { useVModel } from '@vueuse/core';
import { Button, Form, Input, message } from 'ant-design-vue'; import { Button, Form, Input, message } from 'ant-design-vue';
import { IoTDataSpecsDataTypeEnum } from '#/views/iot/utils/constants';
/** 枚举型的 dataSpecs 配置组件 */ /** 枚举型的 dataSpecs 配置组件 */
defineOptions({ name: 'ThingModelEnumDataSpecs' }); defineOptions({ name: 'ThingModelEnumDataSpecs' });
const props = defineProps<{ modelValue: any }>(); const props = defineProps<{ modelValue: any }>();
const emits = defineEmits(['update:modelValue']); const emits = defineEmits(['update:modelValue']);
const dataSpecsList = useVModel(props, 'modelValue', emits) as Ref< const dataSpecsList = useVModel(props, 'modelValue', emits) as Ref<any[]>;
DataSpecsEnumOrBoolData[]
>;
/** 添加枚举项 */ /** 添加枚举项 */
function addEnum() { function addEnum() {
dataSpecsList.value.push({ dataSpecsList.value.push({
dataType: IoTDataSpecsDataTypeEnum.ENUM,
name: '', // 枚举项的名称 name: '', // 枚举项的名称
value: '', // 枚举值 value: '', // 枚举值
}); } as any);
} }
/** 删除枚举项 */ /** 删除枚举项 */
@@ -37,93 +28,10 @@ function deleteEnum(index: number) {
} }
dataSpecsList.value.splice(index, 1); dataSpecsList.value.splice(index, 1);
} }
/** 校验枚举值 */
function validateEnumValue(_: any, value: any, callback: any) {
if (isEmpty(value)) {
callback(new Error('枚举值不能为空'));
return;
}
if (Number.isNaN(Number(value))) {
callback(new Error('枚举值必须是数字'));
return;
}
// 检查枚举值是否重复
const values = dataSpecsList.value.map((item) => item.value);
if (values.filter((v) => v === value).length > 1) {
callback(new Error('枚举值不能重复'));
return;
}
callback();
}
/** 校验枚举描述 */
function validateEnumName(_: any, value: string, callback: any) {
if (isEmpty(value)) {
callback(new Error('枚举描述不能为空'));
return;
}
// 检查开头字符
if (!/^[\u4E00-\u9FA5a-z0-9]/i.test(value)) {
callback(new Error('枚举描述必须以中文、英文字母或数字开头'));
return;
}
// 检查整体格式
if (!/^[\u4E00-\u9FA5a-z0-9][\w\u4E00-\u9FA5-]*$/i.test(value)) {
callback(new Error('枚举描述只能包含中文、英文字母、数字、下划线和短划线'));
return;
}
// 检查长度(一个中文算一个字符)
if (value.length > 20) {
callback(new Error('枚举描述长度不能超过20个字符'));
return;
}
callback();
}
/** 校验整个枚举列表 */
function validateEnumList(_: any, __: any, callback: any) {
if (isEmpty(dataSpecsList.value)) {
callback(new Error('请至少添加一个枚举项'));
return;
}
// 检查是否存在空值
const hasEmptyValue = dataSpecsList.value.some(
(item) => isEmpty(item.value) || isEmpty(item.name),
);
if (hasEmptyValue) {
callback(new Error('存在未填写的枚举值或描述'));
return;
}
// 检查枚举值是否都是数字
const hasInvalidNumber = dataSpecsList.value.some((item) =>
Number.isNaN(Number(item.value)),
);
if (hasInvalidNumber) {
callback(new Error('存在非数字的枚举值'));
return;
}
// 检查是否有重复的枚举值
const values = dataSpecsList.value.map((item) => item.value);
const uniqueValues = new Set(values);
if (values.length !== uniqueValues.size) {
callback(new Error('存在重复的枚举值'));
return;
}
callback();
}
</script> </script>
<template> <template>
<Form.Item <Form.Item label="枚举项">
:rules="[
{ required: true, validator: validateEnumList, trigger: 'change' },
]"
label="枚举项"
>
<div class="flex flex-col"> <div class="flex flex-col">
<div class="flex items-center"> <div class="flex items-center">
<span class="flex-1"> 参数值 </span> <span class="flex-1"> 参数值 </span>
@@ -134,39 +42,25 @@ function validateEnumList(_: any, __: any, callback: any) {
:key="index" :key="index"
class="mb-5px flex items-center justify-between" class="mb-5px flex items-center justify-between"
> >
<Form.Item <div class="flex-1">
:prop="`property.dataSpecsList[${index}].value`" <Input v-model:value="item.value" placeholder="请输入枚举值,如'0'" />
:rules="[ </div>
{ required: true, message: '枚举值不能为空' },
{ validator: validateEnumValue, trigger: 'blur' },
]"
class="mb-0 flex-1"
>
<Input v-model="item.value" placeholder="请输入枚举值,如'0'" />
</Form.Item>
<span class="mx-2">~</span> <span class="mx-2">~</span>
<Form.Item <div class="flex-1">
:prop="`property.dataSpecsList[${index}].name`" <Input v-model:value="item.name" placeholder="对该枚举项的描述" />
:rules="[ </div>
{ required: true, message: '枚举描述不能为空' }, <Button class="ml-10px" type="link" @click="deleteEnum(index)">
{ validator: validateEnumName, trigger: 'blur' },
]"
class="mb-0 flex-1"
>
<Input v-model="item.name" placeholder="对该枚举项的描述" />
</Form.Item>
<Button class="ml-10px" link type="primary" @click="deleteEnum(index)">
删除 删除
</Button> </Button>
</div> </div>
<Button link type="primary" @click="addEnum">+添加枚举项</Button> <Button type="link" @click="addEnum">+添加枚举项</Button>
</div> </div>
</Form.Item> </Form.Item>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
:deep(.el-form-item) { :deep(.ant-form-item) {
.el-form-item { .ant-form-item {
margin-bottom: 0; margin-bottom: 0;
} }
} }

View File

@@ -8,6 +8,7 @@ import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks'; import { getDictOptions } from '@vben/hooks';
import { useVModel } from '@vueuse/core'; import { useVModel } from '@vueuse/core';
import { Form, Input, Select } from 'ant-design-vue';
/** 数值型的 dataSpecs 配置组件 */ /** 数值型的 dataSpecs 配置组件 */
defineOptions({ name: 'ThingModelNumberDataSpecs' }); defineOptions({ name: 'ThingModelNumberDataSpecs' });
@@ -21,132 +22,56 @@ const dataSpecs = useVModel(
) as Ref<DataSpecsNumberData>; ) as Ref<DataSpecsNumberData>;
/** 单位发生变化时触发 */ /** 单位发生变化时触发 */
const unitChange = (UnitSpecs: string) => { const unitChange = (UnitSpecs: any) => {
const [unitName, unit] = UnitSpecs.split('-'); if (!UnitSpecs) return;
const [unitName, unit] = String(UnitSpecs).split('-');
dataSpecs.value.unitName = unitName; dataSpecs.value.unitName = unitName;
dataSpecs.value.unit = unit; dataSpecs.value.unit = unit;
}; };
/** 校验最小值 */
const validateMin = (_: any, __: any, callback: any) => {
const min = Number(dataSpecs.value.min);
const max = Number(dataSpecs.value.max);
if (Number.isNaN(min)) {
callback(new Error('请输入有效的数值'));
return;
}
if (max !== undefined && !Number.isNaN(max) && min >= max) {
callback(new Error('最小值必须小于最大值'));
return;
}
callback();
};
/** 校验最大值 */
const validateMax = (_: any, __: any, callback: any) => {
const min = Number(dataSpecs.value.min);
const max = Number(dataSpecs.value.max);
if (Number.isNaN(max)) {
callback(new Error('请输入有效的数值'));
return;
}
if (min !== undefined && !Number.isNaN(min) && max <= min) {
callback(new Error('最大值必须大于最小值'));
return;
}
callback();
};
/** 校验步长 */
const validateStep = (_: any, __: any, callback: any) => {
const step = Number(dataSpecs.value.step);
if (Number.isNaN(step)) {
callback(new Error('请输入有效的数值'));
return;
}
if (step <= 0) {
callback(new Error('步长必须大于0'));
return;
}
const min = Number(dataSpecs.value.min);
const max = Number(dataSpecs.value.max);
if (!Number.isNaN(min) && !Number.isNaN(max) && step > max - min) {
callback(new Error('步长不能大于最大值和最小值的差值'));
return;
}
callback();
};
</script> </script>
<template> <template>
<el-form-item label="取值范围"> <Form.Item label="取值范围">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<el-form-item <div class="flex-1">
:rules="[ <Input v-model:value="dataSpecs.min" placeholder="请输入最小值" />
{ required: true, message: '最小值不能为空' },
{ validator: validateMin, trigger: 'blur' },
]"
class="mb-0"
prop="property.dataSpecs.min"
>
<el-input v-model="dataSpecs.min" placeholder="请输入最小值" />
</el-form-item>
<span class="mx-2">~</span>
<el-form-item
:rules="[
{ required: true, message: '最大值不能为空' },
{ validator: validateMax, trigger: 'blur' },
]"
class="mb-0"
prop="property.dataSpecs.max"
>
<el-input v-model="dataSpecs.max" placeholder="请输入最大值" />
</el-form-item>
</div> </div>
</el-form-item> <span class="mx-2">~</span>
<el-form-item <div class="flex-1">
:rules="[ <Input v-model:value="dataSpecs.max" placeholder="请输入最大值" />
{ required: true, message: '步长不能为空' }, </div>
{ validator: validateStep, trigger: 'blur' }, </div>
]" </Form.Item>
label="步长" <Form.Item label="步长">
prop="property.dataSpecs.step" <Input v-model:value="dataSpecs.step" placeholder="请输入步长" />
> </Form.Item>
<el-input v-model="dataSpecs.step" placeholder="请输入步长" /> <Form.Item label="单位">
</el-form-item> <Select
<el-form-item
:rules="[{ required: true, message: '请选择单位' }]"
label="单位"
prop="property.dataSpecs.unit"
>
<el-select
:model-value=" :model-value="
dataSpecs.unit ? `${dataSpecs.unitName}-${dataSpecs.unit}` : '' dataSpecs.unit ? `${dataSpecs.unitName}-${dataSpecs.unit}` : ''
" "
filterable show-search
placeholder="请选择单位" placeholder="请选择单位"
class="w-1/1" class="w-1/1"
@change="unitChange" @change="unitChange"
> >
<el-option <Select.Option
v-for="(item, index) in getDictOptions( v-for="(item, index) in getDictOptions(
DICT_TYPE.IOT_THING_MODEL_UNIT, DICT_TYPE.IOT_THING_MODEL_UNIT,
'string', 'string',
)" )"
:key="index" :key="index"
:label="`${item.label}-${item.value}`"
:value="`${item.label}-${item.value}`" :value="`${item.label}-${item.value}`"
/> >
</el-select> {{ `${item.label}-${item.value}` }}
</el-form-item> </Select.Option>
</Select>
</Form.Item>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
:deep(.el-form-item) { :deep(.ant-form-item) {
.el-form-item { .ant-form-item {
margin-bottom: 0; margin-bottom: 0;
} }
} }

View File

@@ -29,6 +29,7 @@ const formData = ref<any>({
dataSpecs: { dataSpecs: {
dataType: IoTDataSpecsDataTypeEnum.INT, dataType: IoTDataSpecsDataTypeEnum.INT,
}, },
dataSpecsList: [],
}, },
}); });
@@ -40,16 +41,22 @@ function openStructForm(val: any) {
return; return;
} }
// 编辑时回显数据 // 编辑时回显数据
const valData = val as any;
formData.value = { formData.value = {
identifier: val.identifier, identifier: valData?.identifier || '',
name: val.name, name: valData?.name || '',
description: val.description, description: valData?.description || '',
property: { property: {
dataType: val.childDataType, dataType: valData?.childDataType || IoTDataSpecsDataTypeEnum.INT,
dataSpecs: val.dataSpecs, dataSpecs: valData?.dataSpecs ?? {},
dataSpecsList: val.dataSpecsList, dataSpecsList: valData?.dataSpecsList ?? [],
}, },
}; };
// 确保 property.dataType 有值
if (!formData.value.property.dataType) {
formData.value.property.dataType = IoTDataSpecsDataTypeEnum.INT;
}
} }
/** 删除 struct 项 */ /** 删除 struct 项 */
@@ -102,20 +109,12 @@ function resetForm() {
dataSpecs: { dataSpecs: {
dataType: IoTDataSpecsDataTypeEnum.INT, dataType: IoTDataSpecsDataTypeEnum.INT,
}, },
dataSpecsList: [],
}, },
}; };
structFormRef.value?.resetFields(); structFormRef.value?.resetFields();
} }
/** 校验 struct 不能为空 */
function validateList(_: any, __: any, callback: any) {
if (isEmpty(dataSpecsList.value)) {
callback(new Error('struct 不能为空'));
return;
}
callback();
}
/** 组件初始化 */ /** 组件初始化 */
onMounted(async () => { onMounted(async () => {
await nextTick(); await nextTick();
@@ -126,51 +125,45 @@ onMounted(async () => {
<template> <template>
<!-- struct 数据展示 --> <!-- struct 数据展示 -->
<Form.Item <Form.Item label="属性对象">
:rules="[{ required: true, validator: validateList, trigger: 'change' }]"
label="JSON 对象"
>
<div <div
v-for="(item, index) in dataSpecsList" v-for="(item, index) in dataSpecsList"
:key="index" :key="index"
class="px-10px mb-10px flex w-full justify-between bg-gray-100" class="px-10px mb-10px flex w-full justify-between bg-gray-100"
> >
<span>参数名称{{ item.name }}</span> <span>参数{{ item.name }}</span>
<div class="btn"> <div class="btn">
<Button link type="primary" @click="openStructForm(item)"> <Button type="link" @click="openStructForm(item)"> 编辑 </Button>
编辑 <Divider type="vertical" />
<Button type="link" danger @click="deleteStructItem(index)">
删除
</Button> </Button>
<Divider direction="vertical" />
<Button link danger @click="deleteStructItem(index)"> 删除 </Button>
</div> </div>
</div> </div>
<Button link type="primary" @click="openStructForm(null)"> <Button type="link" @click="openStructForm(null)"> +新增参数 </Button>
+新增参数
</Button>
</Form.Item> </Form.Item>
<!-- struct 表单 --> <!-- struct 表单 -->
<Modal v-model="dialogVisible" :title="dialogTitle" append-to-body> <Modal
v-model:open="dialogVisible"
:title="dialogTitle"
:confirm-loading="formLoading"
@ok="submitForm"
>
<Form <Form
ref="structFormRef" ref="structFormRef"
v-loading="formLoading"
:model="formData" :model="formData"
label-width="100px" :label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
> >
<Form.Item label="参数名称" prop="name"> <Form.Item label="参数名称" name="name">
<Input v-model="formData.name" placeholder="请输入功能名称" /> <Input v-model:value="formData.name" placeholder="请输入功能名称" />
</Form.Item> </Form.Item>
<Form.Item label="标识符" prop="identifier"> <Form.Item label="标识符" name="identifier">
<Input v-model="formData.identifier" placeholder="请输入标识符" /> <Input v-model:value="formData.identifier" placeholder="请输入标识符" />
</Form.Item> </Form.Item>
<!-- 属性配置 --> <!-- 属性配置 -->
<ThingModelProperty v-model="formData.property" is-struct-data-specs /> <ThingModelProperty v-model="formData.property" is-struct-data-specs />
</Form> </Form>
<template #footer>
<Button :disabled="formLoading" type="primary" @click="submitForm">
</Button>
<Button @click="dialogVisible = false"> </Button>
</template>
</Modal> </Modal>
</template> </template>

View File

@@ -23,7 +23,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '商品', label: '商品',
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
api: getSpuSimpleList, api: () => getSpuSimpleList(),
labelField: 'name', labelField: 'name',
valueField: 'id', valueField: 'id',
placeholder: '请选择商品', placeholder: '请选择商品',

View File

@@ -181,7 +181,7 @@ export function useDeliveryFormSchema(): VbenFormSchema[] {
label: '运费模板', label: '运费模板',
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
api: getSimpleTemplateList, api: () => getSimpleTemplateList(),
labelField: 'name', labelField: 'name',
valueField: 'id', valueField: 'id',
}, },

Some files were not shown because too many files have changed in this diff Show More