19 Commits

Author SHA1 Message Date
YunaiV
b42e9b36e5 feat(wms):优化 antd、ele 的 order receipt 迁移 2026-05-18 01:02:09 +08:00
YunaiV
f8c2d4b1ff feat(wms):优化 antd、ele 的 order receipt 迁移 2026-05-17 23:56:05 +08:00
YunaiV
41d5aa93d6 feat(wms):新增 ele 的 order receipt 迁移 2026-05-17 23:40:00 +08:00
YunaiV
3135b28211 feat(全局):增加 number-range-input 组件 2026-05-17 23:35:31 +08:00
YunaiV
08511191f7 feat(wms):修复 print 顶部白块的问题,对齐 vue3 + ep 的样式 2026-05-17 23:15:16 +08:00
YunaiV
0246fa1ebc feat(wms):优化 order receipt 的实现,对齐 vue3 + ep 版本 2026-05-17 23:09:18 +08:00
YunaiV
0e4012c623 feat(全局):增加 barcode 二维码组件 2026-05-17 23:07:56 +08:00
YunaiV
4933180560 feat(wms):增加 receipt 功能、评审 2026-05-17 21:39:15 +08:00
YunaiV
8710da9383 feat(wms):增加 wms 工具类 2026-05-17 19:09:01 +08:00
YunaiV
5a1f4901da feat(iot):优化 iot 设备管理的样式 2026-05-17 19:07:50 +08:00
YunaiV
3da4a3f417 feat(wms):将首页的枚举值去掉,统一合并到 constants 里,更聚焦点 2026-05-17 18:17:30 +08:00
YunaiV
84b91c6795 feat(iot):优化 iot 产品管理的样式 2026-05-17 18:11:31 +08:00
YunaiV
735ff018be feat(wms):增加 home 统计的迁移 2026-05-17 17:48:53 +08:00
YunaiV
0163794e3f feat(wms):增加 category 模块的迁移 2026-05-17 16:47:27 +08:00
YunaiV
bb63ca9541 feat(wms):增加 brand 模块的迁移 2026-05-17 16:35:51 +08:00
YunaiV
6b28518165 feat(wms):迁移 api 接口 2026-05-17 16:30:50 +08:00
YunaiV
4adce844d3 feat(wms):增加 merchant 模块的迁移 2026-05-17 10:50:34 +08:00
YunaiV
19b5f38e23 feat(wms):增加 warehouse 模块的迁移 2026-05-16 23:12:33 +08:00
YunaiV
80fa8b74e8 feat:补齐 antd 的 component: 'InputNumber', 的 class full 样式 2026-05-16 22:46:16 +08:00
208 changed files with 12263 additions and 1255 deletions

View File

@@ -1,53 +1,9 @@
import type { PageParam, PageResult } from '@vben/request';
import { isEmpty } from '@vben/utils';
import { requestClient } from '#/api/request';
export namespace ThingModelApi {
/** IoT 物模型数据 VO */
export interface ThingModel {
id?: number;
productId?: number;
productKey?: string;
identifier: string;
name: string;
desc?: string;
type: string;
property?: ThingModelProperty;
event?: ThingModelEvent;
service?: ThingModelService;
}
/** IoT 物模型属性 */
export interface Property {
identifier: string;
name: string;
accessMode: string;
dataType: string;
dataSpecs?: any;
dataSpecsList?: any[];
desc?: string;
}
/** IoT 物模型服务 */
export interface Service {
identifier: string;
name: string;
callType: string;
inputData?: any[];
outputData?: any[];
desc?: string;
}
/** IoT 物模型事件 */
export interface Event {
identifier: string;
name: string;
type: string;
outputData?: any[];
desc?: string;
}
}
/** IoT 物模型数据 */
export interface ThingModelData {
id?: number;
@@ -55,9 +11,9 @@ export interface ThingModelData {
productKey?: string;
identifier?: string;
name?: string;
desc?: string;
type?: string;
description?: string;
dataType?: string;
type?: number; // 参见 IoTThingModelTypeEnum 枚举类
property?: ThingModelProperty;
event?: ThingModelEvent;
service?: ThingModelService;
@@ -68,29 +24,45 @@ export interface ThingModelProperty {
identifier?: string;
name?: string;
accessMode?: string;
required?: boolean;
dataType?: string;
description?: string;
dataSpecs?: any;
dataSpecsList?: any[];
desc?: string;
}
/** IoT 物模型服务 */
export interface ThingModelService {
identifier?: string;
name?: string;
required?: boolean;
callType?: string;
inputData?: any[];
outputData?: any[];
desc?: string;
description?: string;
inputParams?: ThingModelParam[];
outputParams?: ThingModelParam[];
method?: string;
}
/** IoT 物模型事件 */
export interface ThingModelEvent {
identifier?: string;
name?: string;
required?: boolean;
type?: string;
outputData?: any[];
desc?: string;
description?: string;
outputParams?: ThingModelParam[];
method?: string;
}
/** IoT 物模型参数 */
export interface ThingModelParam {
identifier?: string;
name?: string;
direction?: string;
paraOrder?: number;
dataType?: string;
dataSpecs?: any;
dataSpecsList?: any[];
}
/** IoT 数据定义(数值型) */
@@ -108,23 +80,119 @@ export interface DataSpecsEnumOrBoolData {
name: string;
}
/** IoT 物模型表单校验规则 */
export interface ThingModelFormRules {
[key: string]: any;
/** 生成「必填 + 数字」类校验器:拼到 size / length / 枚举值上 */
function buildRequiredNumberValidator(label: string) {
return (_rule: any, value: any, callback: any) => {
if (isEmpty(value)) {
callback(new Error(`${label}不能为空`));
return;
}
if (Number.isNaN(Number(value))) {
callback(new Error(`${label}必须是数字`));
return;
}
callback();
};
}
/** 验证布尔型名称 */
export function validateBoolName(_rule: any, value: any, callback: any) {
if (value) {
/** 生成「标识符样式」名称校验器:开头需为中文 / 英文 / 数字,整体仅允许中文、英文、数字、下划线、短划线,长度 ≤ 20 */
export function buildIdentifierLikeNameValidator(label: string) {
return (_rule: any, value: string, callback: any) => {
if (isEmpty(value)) {
callback(new Error(`${label}不能为空`));
return;
}
if (!/^[一-龥A-Za-z0-9]/.test(value)) {
callback(new Error(`${label}必须以中文、英文字母或数字开头`));
return;
}
if (!/^[一-龥A-Za-z0-9][\w一-龥-]*$/.test(value)) {
callback(
new Error(`${label}只能包含中文、英文字母、数字、下划线和短划线`),
);
return;
}
if (value.length > 20) {
callback(new Error(`${label}长度不能超过 20 个字符`));
return;
}
callback();
} else {
callback(new Error('枚举描述不能为空'));
}
};
}
/** IoT 物模型表单校验规则 */
export const ThingModelFormRules = {
name: [
{ required: true, message: '功能名称不能为空', trigger: 'blur' },
{
pattern: /^[一-龥A-Za-z0-9][一-龥A-Za-z0-9\-_/.]{0,29}$/,
message:
'支持中文、大小写字母、日文、数字、短划线、下划线、斜杠和小数点,必须以中文、英文或数字开头,不超过 30 个字符',
trigger: 'blur',
},
],
type: [{ required: true, message: '功能类型不能为空', trigger: 'blur' }],
identifier: [
{ required: true, message: '标识符不能为空', trigger: 'blur' },
{
pattern: /^\w{1,50}$/,
message: '支持大小写字母、数字和下划线,不超过 50 个字符',
trigger: 'blur',
},
{
validator: (_rule: any, value: string, callback: any) => {
const reservedKeywords = [
'set',
'get',
'post',
'property',
'event',
'time',
'value',
];
if (reservedKeywords.includes(value)) {
callback(
new Error(
'set, get, post, property, event, time, value 是系统保留字段,不能用于标识符定义',
),
);
return;
}
if (/^\d+$/.test(value)) {
callback(new Error('标识符不能是纯数字'));
return;
}
callback();
},
trigger: 'blur',
},
],
childDataType: [{ required: true, message: '元素类型不能为空' }],
size: [
{
required: true,
validator: buildRequiredNumberValidator('元素个数'),
trigger: 'blur',
},
],
length: [
{
required: true,
validator: buildRequiredNumberValidator('文本长度'),
trigger: 'blur',
},
],
accessMode: [{ required: true, message: '请选择读写类型', trigger: 'change' }],
callType: [{ required: true, message: '请选择调用方式', trigger: 'change' }],
eventType: [{ required: true, message: '请选择事件类型', trigger: 'change' }],
};
/** 校验布尔值名称 */
export const validateBoolName = buildIdentifierLikeNameValidator('布尔值名称');
/** 查询产品物模型分页 */
export function getThingModelPage(params: PageParam) {
return requestClient.get<PageResult<ThingModelApi.ThingModel>>(
return requestClient.get<PageResult<ThingModelData>>(
'/iot/thing-model/page',
{ params },
);
@@ -132,17 +200,14 @@ export function getThingModelPage(params: PageParam) {
/** 查询产品物模型详情 */
export function getThingModel(id: number) {
return requestClient.get<ThingModelApi.ThingModel>(
`/iot/thing-model/get?id=${id}`,
);
return requestClient.get<ThingModelData>(`/iot/thing-model/get?id=${id}`);
}
/** 根据产品 ID 查询物模型列表 */
export function getThingModelListByProductId(productId: number) {
return requestClient.get<ThingModelApi.ThingModel[]>(
'/iot/thing-model/list',
{ params: { productId } },
);
return requestClient.get<ThingModelData[]>('/iot/thing-model/list', {
params: { productId },
});
}
/** 新增物模型 */
@@ -162,25 +227,7 @@ export function deleteThingModel(id: number) {
/** 获取物模型 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) {
return requestClient.post('/iot/thing-model/import-tsl', {
productId,
tslData,
});
}
*/
/** 导出物模型 TSL
export function exportThingModelTSL(productId: number) {
return requestClient.get<any>('/iot/thing-model/export-tsl', {
return requestClient.get<any>('/iot/thing-model/get-tsl', {
params: { productId },
});
}
*/

View File

@@ -0,0 +1,66 @@
import { requestClient } from '#/api/request';
export namespace WmsHomeStatisticsApi {
export interface StatisticsReq {
goodsLimit?: number;
warehouseId?: number;
warehouseLimit?: number;
}
export interface OrderStatus {
count: number;
status: number;
}
export interface OrderSummary {
statuses: OrderStatus[];
total: number;
type: number;
}
export interface OrderTrend {
checkCount: number;
movementCount: number;
receiptCount: number;
shipmentCount: number;
time: number | string;
}
export interface InventoryRankItem {
id: number;
name: string;
quantity: number;
}
export interface InventorySummary {
goodsShareList: InventoryRankItem[];
totalQuantity: number;
warehouseDistributionList: InventoryRankItem[];
}
}
export function getOrderSummary(params?: WmsHomeStatisticsApi.StatisticsReq) {
return requestClient.get<WmsHomeStatisticsApi.OrderSummary[]>(
'/wms/home-statistics/order-summary',
{ params },
);
}
export function getOrderTrend(
days?: number,
params?: WmsHomeStatisticsApi.StatisticsReq,
) {
return requestClient.get<WmsHomeStatisticsApi.OrderTrend[]>(
'/wms/home-statistics/order-trend',
{ params: { ...params, days } },
);
}
export function getInventorySummary(
params?: WmsHomeStatisticsApi.StatisticsReq,
) {
return requestClient.get<WmsHomeStatisticsApi.InventorySummary>(
'/wms/home-statistics/inventory-summary',
{ params },
);
}

View File

@@ -0,0 +1,37 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace WmsInventoryHistoryApi {
/** WMS 库存记录 */
export interface InventoryHistory {
id?: number;
itemId?: number;
itemCode?: string;
itemName?: string;
unit?: string;
skuId?: number;
skuCode?: string;
skuName?: string;
warehouseId?: number;
warehouseName?: string;
quantity?: number;
beforeQuantity?: number;
afterQuantity?: number;
price?: number;
totalPrice?: number;
remark?: string;
orderId?: number;
orderNo?: string;
orderType?: number;
createTime?: Date;
}
}
/** 查询库存记录分页 */
export function getInventoryHistoryPage(params: PageParam) {
return requestClient.get<PageResult<WmsInventoryHistoryApi.InventoryHistory>>(
'/wms/inventory-history/page',
{ params },
);
}

View File

@@ -0,0 +1,42 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace WmsInventoryApi {
/** WMS 库存统计 */
export interface Inventory {
id?: number;
itemId?: number;
itemCode?: string;
itemName?: string;
unit?: string;
skuId?: number;
skuCode?: string;
skuName?: string;
warehouseId?: number;
warehouseName?: string;
quantity?: number;
remark?: string;
createTime?: Date;
}
/** WMS 库存统计列表请求 */
export interface InventoryListReq {
warehouseId: number;
}
}
/** 查询库存统计分页 */
export function getInventoryPage(params: PageParam) {
return requestClient.get<PageResult<WmsInventoryApi.Inventory>>(
'/wms/inventory/page',
{ params },
);
}
/** 查询库存统计列表 */
export function getInventoryList(params: WmsInventoryApi.InventoryListReq) {
return requestClient.get<WmsInventoryApi.Inventory[]>('/wms/inventory/list', {
params,
});
}

View File

@@ -0,0 +1,55 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace WmsItemBrandApi {
/** WMS 商品品牌 */
export interface ItemBrand {
id?: number;
code?: string;
name?: string;
createTime?: Date;
}
}
/** 查询商品品牌分页 */
export function getItemBrandPage(params: PageParam) {
return requestClient.get<PageResult<WmsItemBrandApi.ItemBrand>>(
'/wms/item-brand/page',
{ params },
);
}
/** 查询商品品牌精简列表 */
export function getItemBrandSimpleList() {
return requestClient.get<WmsItemBrandApi.ItemBrand[]>(
'/wms/item-brand/simple-list',
);
}
/** 查询商品品牌详情 */
export function getItemBrand(id: number) {
return requestClient.get<WmsItemBrandApi.ItemBrand>(
`/wms/item-brand/get?id=${id}`,
);
}
/** 新增商品品牌 */
export function createItemBrand(data: WmsItemBrandApi.ItemBrand) {
return requestClient.post('/wms/item-brand/create', data);
}
/** 修改商品品牌 */
export function updateItemBrand(data: WmsItemBrandApi.ItemBrand) {
return requestClient.put('/wms/item-brand/update', data);
}
/** 删除商品品牌 */
export function deleteItemBrand(id: number) {
return requestClient.delete(`/wms/item-brand/delete?id=${id}`);
}
/** 导出商品品牌 */
export function exportItemBrand(params: any) {
return requestClient.download('/wms/item-brand/export-excel', { params });
}

View File

@@ -0,0 +1,52 @@
import { requestClient } from '#/api/request';
export namespace WmsItemCategoryApi {
/** WMS 商品分类 */
export interface ItemCategory {
id?: number;
parentId?: number;
code?: string;
name?: string;
sort?: number;
status?: number;
createTime?: Date;
children?: ItemCategory[];
}
}
/** 查询商品分类列表 */
export function getItemCategoryList(params?: any) {
return requestClient.get<WmsItemCategoryApi.ItemCategory[]>(
'/wms/item-category/list',
{ params },
);
}
/** 查询商品分类精简列表 */
export function getItemCategorySimpleList() {
return requestClient.get<WmsItemCategoryApi.ItemCategory[]>(
'/wms/item-category/simple-list',
);
}
/** 查询商品分类详情 */
export function getItemCategory(id: number) {
return requestClient.get<WmsItemCategoryApi.ItemCategory>(
`/wms/item-category/get?id=${id}`,
);
}
/** 新增商品分类 */
export function createItemCategory(data: WmsItemCategoryApi.ItemCategory) {
return requestClient.post('/wms/item-category/create', data);
}
/** 修改商品分类 */
export function updateItemCategory(data: WmsItemCategoryApi.ItemCategory) {
return requestClient.put('/wms/item-category/update', data);
}
/** 删除商品分类 */
export function deleteItemCategory(id: number) {
return requestClient.delete(`/wms/item-category/delete?id=${id}`);
}

View File

@@ -0,0 +1,61 @@
import type { PageParam, PageResult } from '@vben/request';
import type { WmsItemSkuApi } from './sku';
import { requestClient } from '#/api/request';
export namespace WmsItemApi {
/** WMS 商品 */
export interface Item {
id?: number;
code?: string;
name?: string;
categoryId?: number;
categoryName?: string;
unit?: string;
brandId?: number;
brandName?: string;
remark?: string;
skus?: WmsItemSkuApi.ItemSku[];
createTime?: Date;
}
}
/** 查询商品分页 */
export function getItemPage(params: PageParam) {
return requestClient.get<PageResult<WmsItemApi.Item>>('/wms/item/page', {
params,
});
}
/** 查询商品精简列表 */
export function getItemSimpleList(params?: any) {
return requestClient.get<WmsItemApi.Item[]>('/wms/item/simple-list', {
params,
});
}
/** 查询商品详情 */
export function getItem(id: number) {
return requestClient.get<WmsItemApi.Item>(`/wms/item/get?id=${id}`);
}
/** 新增商品 */
export function createItem(data: WmsItemApi.Item) {
return requestClient.post('/wms/item/create', data);
}
/** 修改商品 */
export function updateItem(data: WmsItemApi.Item) {
return requestClient.put('/wms/item/update', data);
}
/** 删除商品 */
export function deleteItem(id: number) {
return requestClient.delete(`/wms/item/delete?id=${id}`);
}
/** 导出商品 */
export function exportItem(params: any) {
return requestClient.download('/wms/item/export-excel', { params });
}

View File

@@ -0,0 +1,37 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace WmsItemSkuApi {
/** WMS 商品 SKU */
export interface ItemSku {
id?: number;
name?: string;
itemId?: number;
itemCode?: string;
itemName?: string;
categoryId?: number;
categoryName?: string;
unit?: string;
brandId?: number;
brandName?: string;
barCode?: string;
code?: string;
length?: number;
width?: number;
height?: number;
grossWeight?: number;
netWeight?: number;
costPrice?: number;
sellingPrice?: number;
createTime?: Date;
}
}
/** 按 SKU 维度分页(支持商品 / 品牌 / 分类多表联查筛选) */
export function getItemSkuPage(params: PageParam) {
return requestClient.get<PageResult<WmsItemSkuApi.ItemSku>>(
'/wms/item-sku/page',
{ params },
);
}

View File

@@ -0,0 +1,73 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace WmsMerchantApi {
/** WMS 往来企业 */
export interface Merchant {
id?: number;
code?: string;
name?: string;
type?: number;
level?: string;
bankName?: string;
bankAccount?: string;
address?: string;
mobile?: string;
telephone?: string;
contact?: string;
email?: string;
remark?: string;
createTime?: Date;
}
/** WMS 往来企业精简列表请求 */
export interface MerchantSimpleListReq {
types?: number[];
}
}
/** 查询往来企业分页 */
export function getMerchantPage(params: PageParam) {
return requestClient.get<PageResult<WmsMerchantApi.Merchant>>(
'/wms/merchant/page',
{ params },
);
}
/** 查询往来企业精简列表 */
export function getMerchantSimpleList(
params?: WmsMerchantApi.MerchantSimpleListReq,
) {
return requestClient.get<WmsMerchantApi.Merchant[]>(
'/wms/merchant/simple-list',
{ params },
);
}
/** 查询往来企业详情 */
export function getMerchant(id: number) {
return requestClient.get<WmsMerchantApi.Merchant>(
`/wms/merchant/get?id=${id}`,
);
}
/** 新增往来企业 */
export function createMerchant(data: WmsMerchantApi.Merchant) {
return requestClient.post('/wms/merchant/create', data);
}
/** 修改往来企业 */
export function updateMerchant(data: WmsMerchantApi.Merchant) {
return requestClient.put('/wms/merchant/update', data);
}
/** 删除往来企业 */
export function deleteMerchant(id: number) {
return requestClient.delete(`/wms/merchant/delete?id=${id}`);
}
/** 导出往来企业 */
export function exportMerchant(params: any) {
return requestClient.download('/wms/merchant/export-excel', { params });
}

View File

@@ -0,0 +1,57 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace WmsWarehouseApi {
/** WMS 仓库 */
export interface Warehouse {
id?: number;
code?: string;
name?: string;
remark?: string;
sort?: number;
createTime?: Date;
}
}
/** 查询仓库分页 */
export function getWarehousePage(params: PageParam) {
return requestClient.get<PageResult<WmsWarehouseApi.Warehouse>>(
'/wms/warehouse/page',
{ params },
);
}
/** 查询仓库精简列表 */
export function getWarehouseSimpleList() {
return requestClient.get<WmsWarehouseApi.Warehouse[]>(
'/wms/warehouse/simple-list',
);
}
/** 查询仓库详情 */
export function getWarehouse(id: number) {
return requestClient.get<WmsWarehouseApi.Warehouse>(
`/wms/warehouse/get?id=${id}`,
);
}
/** 新增仓库 */
export function createWarehouse(data: WmsWarehouseApi.Warehouse) {
return requestClient.post('/wms/warehouse/create', data);
}
/** 修改仓库 */
export function updateWarehouse(data: WmsWarehouseApi.Warehouse) {
return requestClient.put('/wms/warehouse/update', data);
}
/** 删除仓库 */
export function deleteWarehouse(id: number) {
return requestClient.delete(`/wms/warehouse/delete?id=${id}`);
}
/** 导出仓库 */
export function exportWarehouse(params: any) {
return requestClient.download('/wms/warehouse/export-excel', { params });
}

View File

@@ -0,0 +1,23 @@
export namespace WmsCheckOrderDetailApi {
/** WMS 盘库单明细 */
export interface CheckOrderDetail {
id?: number;
orderId?: number;
itemId?: number;
itemCode?: string;
itemName?: string;
unit?: string;
skuId?: number;
skuCode?: string;
skuName?: string;
inventoryId?: number;
warehouseId?: number;
warehouseName?: string;
receiptTime?: Date;
quantity?: number;
checkQuantity?: number;
availableQuantity?: number;
price?: number;
createTime?: Date;
}
}

View File

@@ -0,0 +1,71 @@
import type { PageParam, PageResult } from '@vben/request';
import type { WmsCheckOrderDetailApi } from './detail';
import { requestClient } from '#/api/request';
export namespace WmsCheckOrderApi {
/** WMS 盘库单 */
export interface CheckOrder {
id?: number;
no?: string;
orderTime?: string;
status?: number;
remark?: string;
warehouseId?: number;
warehouseName?: string;
totalQuantity?: number;
totalPrice?: number;
actualPrice?: number;
details?: WmsCheckOrderDetailApi.CheckOrderDetail[];
createTime?: Date;
creator?: string;
creatorName?: string;
updateTime?: Date;
updater?: string;
updaterName?: string;
}
}
export function getCheckOrderPage(params: PageParam) {
return requestClient.get<PageResult<WmsCheckOrderApi.CheckOrder>>(
'/wms/check-order/page',
{ params },
);
}
export function getCheckOrder(id: number) {
return requestClient.get<WmsCheckOrderApi.CheckOrder>(
`/wms/check-order/get?id=${id}`,
);
}
export function getCheckOrderDetailListByOrderId(orderId: number) {
return requestClient.get<WmsCheckOrderDetailApi.CheckOrderDetail[]>(
`/wms/check-order-detail/list-by-order-id?orderId=${orderId}`,
);
}
export function createCheckOrder(data: WmsCheckOrderApi.CheckOrder) {
return requestClient.post('/wms/check-order/create', data);
}
export function updateCheckOrder(data: WmsCheckOrderApi.CheckOrder) {
return requestClient.put('/wms/check-order/update', data);
}
export function completeCheckOrder(id: number) {
return requestClient.put(`/wms/check-order/complete?id=${id}`);
}
export function cancelCheckOrder(id: number) {
return requestClient.put(`/wms/check-order/cancel?id=${id}`);
}
export function deleteCheckOrder(id: number) {
return requestClient.delete(`/wms/check-order/delete?id=${id}`);
}
export function exportCheckOrder(params: any) {
return requestClient.download('/wms/check-order/export-excel', { params });
}

View File

@@ -0,0 +1,23 @@
export namespace WmsMovementOrderDetailApi {
/** WMS 移库单明细 */
export interface MovementOrderDetail {
id?: number;
orderId?: number;
itemId?: number;
itemCode?: string;
itemName?: string;
unit?: string;
skuId?: number;
skuCode?: string;
skuName?: string;
sourceWarehouseId?: number;
sourceWarehouseName?: string;
targetWarehouseId?: number;
targetWarehouseName?: string;
quantity?: number;
availableQuantity?: number;
price?: number;
totalPrice?: number;
createTime?: Date;
}
}

View File

@@ -0,0 +1,72 @@
import type { PageParam, PageResult } from '@vben/request';
import type { WmsMovementOrderDetailApi } from './detail';
import { requestClient } from '#/api/request';
export namespace WmsMovementOrderApi {
/** WMS 移库单 */
export interface MovementOrder {
id?: number;
no?: string;
orderTime?: string;
status?: number;
remark?: string;
sourceWarehouseId?: number;
sourceWarehouseName?: string;
targetWarehouseId?: number;
targetWarehouseName?: string;
totalQuantity?: number;
totalPrice?: number;
details?: WmsMovementOrderDetailApi.MovementOrderDetail[];
createTime?: Date;
creator?: string;
creatorName?: string;
updateTime?: Date;
updater?: string;
updaterName?: string;
}
}
export function getMovementOrderPage(params: PageParam) {
return requestClient.get<PageResult<WmsMovementOrderApi.MovementOrder>>(
'/wms/movement-order/page',
{ params },
);
}
export function getMovementOrder(id: number) {
return requestClient.get<WmsMovementOrderApi.MovementOrder>(
`/wms/movement-order/get?id=${id}`,
);
}
export function getMovementOrderDetailListByOrderId(orderId: number) {
return requestClient.get<WmsMovementOrderDetailApi.MovementOrderDetail[]>(
`/wms/movement-order-detail/list-by-order-id?orderId=${orderId}`,
);
}
export function createMovementOrder(data: WmsMovementOrderApi.MovementOrder) {
return requestClient.post('/wms/movement-order/create', data);
}
export function updateMovementOrder(data: WmsMovementOrderApi.MovementOrder) {
return requestClient.put('/wms/movement-order/update', data);
}
export function completeMovementOrder(id: number) {
return requestClient.put(`/wms/movement-order/complete?id=${id}`);
}
export function cancelMovementOrder(id: number) {
return requestClient.put(`/wms/movement-order/cancel?id=${id}`);
}
export function deleteMovementOrder(id: number) {
return requestClient.delete(`/wms/movement-order/delete?id=${id}`);
}
export function exportMovementOrder(params: any) {
return requestClient.download('/wms/movement-order/export-excel', { params });
}

View File

@@ -0,0 +1,20 @@
export namespace WmsReceiptOrderDetailApi {
/** WMS 入库单明细 */
export interface ReceiptOrderDetail {
id?: number;
orderId?: number;
itemId?: number;
itemCode?: string;
itemName?: string;
unit?: string;
skuId?: number;
skuCode?: string;
skuName?: string;
warehouseId?: number;
warehouseName?: string;
quantity?: number;
price?: number;
totalPrice?: number;
createTime?: Date;
}
}

View File

@@ -0,0 +1,74 @@
import type { PageParam, PageResult } from '@vben/request';
import type { WmsReceiptOrderDetailApi } from './detail';
import { requestClient } from '#/api/request';
export namespace WmsReceiptOrderApi {
/** WMS 入库单 */
export interface ReceiptOrder {
id?: number;
no?: string;
type?: number;
orderTime?: string;
status?: number;
bizOrderNo?: string;
merchantId?: number;
merchantName?: string;
remark?: string;
warehouseId?: number;
warehouseName?: string;
totalQuantity?: number;
totalPrice?: number;
details?: WmsReceiptOrderDetailApi.ReceiptOrderDetail[];
createTime?: Date;
creator?: string;
creatorName?: string;
updateTime?: Date;
updater?: string;
updaterName?: string;
}
}
export function getReceiptOrderPage(params: PageParam) {
return requestClient.get<PageResult<WmsReceiptOrderApi.ReceiptOrder>>(
'/wms/receipt-order/page',
{ params },
);
}
export function getReceiptOrder(id: number) {
return requestClient.get<WmsReceiptOrderApi.ReceiptOrder>(
`/wms/receipt-order/get?id=${id}`,
);
}
export function getReceiptOrderDetailListByOrderId(orderId: number) {
return requestClient.get<WmsReceiptOrderDetailApi.ReceiptOrderDetail[]>(
`/wms/receipt-order-detail/list-by-order-id?orderId=${orderId}`,
);
}
export function createReceiptOrder(data: WmsReceiptOrderApi.ReceiptOrder) {
return requestClient.post('/wms/receipt-order/create', data);
}
export function updateReceiptOrder(data: WmsReceiptOrderApi.ReceiptOrder) {
return requestClient.put('/wms/receipt-order/update', data);
}
export function completeReceiptOrder(id: number) {
return requestClient.put(`/wms/receipt-order/complete?id=${id}`);
}
export function cancelReceiptOrder(id: number) {
return requestClient.put(`/wms/receipt-order/cancel?id=${id}`);
}
export function deleteReceiptOrder(id: number) {
return requestClient.delete(`/wms/receipt-order/delete?id=${id}`);
}
export function exportReceiptOrder(params: any) {
return requestClient.download('/wms/receipt-order/export-excel', { params });
}

View File

@@ -0,0 +1,21 @@
export namespace WmsShipmentOrderDetailApi {
/** WMS 出库单明细 */
export interface ShipmentOrderDetail {
id?: number;
orderId?: number;
itemId?: number;
itemCode?: string;
itemName?: string;
unit?: string;
skuId?: number;
skuCode?: string;
skuName?: string;
warehouseId?: number;
warehouseName?: string;
quantity?: number;
availableQuantity?: number;
price?: number;
totalPrice?: number;
createTime?: Date;
}
}

View File

@@ -0,0 +1,74 @@
import type { PageParam, PageResult } from '@vben/request';
import type { WmsShipmentOrderDetailApi } from './detail';
import { requestClient } from '#/api/request';
export namespace WmsShipmentOrderApi {
/** WMS 出库单 */
export interface ShipmentOrder {
id?: number;
no?: string;
type?: number;
orderTime?: string;
status?: number;
bizOrderNo?: string;
merchantId?: number;
merchantName?: string;
remark?: string;
warehouseId?: number;
warehouseName?: string;
totalQuantity?: number;
totalPrice?: number;
details?: WmsShipmentOrderDetailApi.ShipmentOrderDetail[];
createTime?: Date;
creator?: string;
creatorName?: string;
updateTime?: Date;
updater?: string;
updaterName?: string;
}
}
export function getShipmentOrderPage(params: PageParam) {
return requestClient.get<PageResult<WmsShipmentOrderApi.ShipmentOrder>>(
'/wms/shipment-order/page',
{ params },
);
}
export function getShipmentOrder(id: number) {
return requestClient.get<WmsShipmentOrderApi.ShipmentOrder>(
`/wms/shipment-order/get?id=${id}`,
);
}
export function getShipmentOrderDetailListByOrderId(orderId: number) {
return requestClient.get<WmsShipmentOrderDetailApi.ShipmentOrderDetail[]>(
`/wms/shipment-order-detail/list-by-order-id?orderId=${orderId}`,
);
}
export function createShipmentOrder(data: WmsShipmentOrderApi.ShipmentOrder) {
return requestClient.post('/wms/shipment-order/create', data);
}
export function updateShipmentOrder(data: WmsShipmentOrderApi.ShipmentOrder) {
return requestClient.put('/wms/shipment-order/update', data);
}
export function completeShipmentOrder(id: number) {
return requestClient.put(`/wms/shipment-order/complete?id=${id}`);
}
export function cancelShipmentOrder(id: number) {
return requestClient.put(`/wms/shipment-order/cancel?id=${id}`);
}
export function deleteShipmentOrder(id: number) {
return requestClient.delete(`/wms/shipment-order/delete?id=${id}`);
}
export function exportShipmentOrder(params: any) {
return requestClient.download('/wms/shipment-order/export-excel', { params });
}

View File

@@ -0,0 +1,39 @@
import type { VbenFormSchema } from '#/adapter/form';
import { markRaw } from 'vue';
import NumberRangeInput from './number-range-input.vue';
export { default as NumberRangeInput } from './number-range-input.vue';
export type NumberRangeValue = [number | undefined, number | undefined];
function splitNumberRange(minFieldName: string, maxFieldName: string) {
return (
value: NumberRangeValue | undefined,
setValue: (fieldName: string, value: number | undefined) => void,
) => {
setValue(minFieldName, value?.[0]);
setValue(maxFieldName, value?.[1]);
return undefined;
};
}
export function buildNumberRangeSchema(
label: string,
fieldName: string,
minFieldName: string,
maxFieldName: string,
precision: number,
): VbenFormSchema {
return {
component: markRaw(NumberRangeInput),
componentProps: {
min: 0,
precision,
},
fieldName,
label,
valueFormat: splitNumberRange(minFieldName, maxFieldName),
};
}

View File

@@ -0,0 +1,73 @@
<script lang="ts" setup>
import { InputNumber } from 'ant-design-vue';
type NumberRangeValue = [number | undefined, number | undefined];
const props = withDefaults(
defineProps<{
maxPlaceholder?: string;
min?: number;
minPlaceholder?: string;
precision?: number;
value?: NumberRangeValue;
}>(),
{
maxPlaceholder: '最大值',
min: undefined,
minPlaceholder: '最小值',
precision: 2,
value: undefined,
},
);
const emit = defineEmits<{
'update:value': [value: NumberRangeValue | undefined];
}>();
function normalizeValue(value: unknown) {
if (typeof value === 'number') {
return Number.isFinite(value) ? value : undefined;
}
if (typeof value === 'string' && value.trim() !== '') {
const numberValue = Number(value);
return Number.isFinite(numberValue) ? numberValue : undefined;
}
return undefined;
}
function updateValue(index: 0 | 1, value: unknown) {
const next: NumberRangeValue = [
props.value?.[0] ?? undefined,
props.value?.[1] ?? undefined,
];
next[index] = normalizeValue(value);
emit(
'update:value',
next[0] === undefined && next[1] === undefined ? undefined : next,
);
}
</script>
<template>
<div class="flex w-full items-center gap-2">
<InputNumber
:controls="false"
:min="min"
:placeholder="minPlaceholder"
:precision="precision"
:value="value?.[0]"
class="min-w-0 flex-1"
@update:value="updateValue(0, $event)"
/>
<span class="shrink-0 text-muted-foreground"></span>
<InputNumber
:controls="false"
:min="min"
:placeholder="maxPlaceholder"
:precision="precision"
:value="value?.[1]"
class="min-w-0 flex-1"
@update:value="updateValue(1, $event)"
/>
</div>
</template>

View File

@@ -41,6 +41,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '温度参数',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入温度参数',
precision: 2,
min: 0,
@@ -53,6 +54,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '回复数 Token 数',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入回复数 Token 数',
min: 0,
max: 8192,
@@ -64,6 +66,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '上下文数量',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入上下文数量',
min: 0,
max: 20,

View File

@@ -52,6 +52,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '检索 topK',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入检索 topK',
min: 0,
max: 10,
@@ -63,6 +64,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '检索相似度阈值',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入检索相似度阈值',
min: 0,
max: 1,

View File

@@ -55,6 +55,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '检索 topK',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入检索 topK',
min: 0,
max: 10,
@@ -66,6 +67,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '检索相似度阈值',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入检索相似度阈值',
min: 0,
max: 1,

View File

@@ -154,6 +154,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '角色排序',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入角色排序',
},
dependencies: {

View File

@@ -84,6 +84,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '模型排序',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入模型排序',
},
rules: 'required',
@@ -104,6 +105,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '温度参数',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入温度参数',
min: 0,
max: 2,
@@ -121,6 +123,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '回复数 Token 数',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
max: 8192,
placeholder: '请输入回复数 Token 数',
@@ -138,6 +141,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '上下文数量',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
max: 20,
placeholder: '请输入上下文数量',

View File

@@ -60,6 +60,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '分类排序',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
placeholder: '请输入分类排序',
},

View File

@@ -118,6 +118,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '产品总金额',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
precision: 2,
disabled: true,
@@ -130,6 +131,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '整单折扣(%',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
precision: 2,
placeholder: '请输入整单折扣',
@@ -141,6 +143,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '折扣后金额',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
precision: 2,
disabled: true,

View File

@@ -17,6 +17,7 @@ export const schema: VbenFormSchema[] = [
component: 'InputNumber',
fieldName: 'notifyDays',
componentProps: {
class: '!w-full',
min: 0,
precision: 0,
},

View File

@@ -198,6 +198,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '产品总金额',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
precision: 2,
placeholder: '请输入产品总金额',
@@ -209,6 +210,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '整单折扣(%',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
precision: 2,
placeholder: '请输入整单折扣',
@@ -220,6 +222,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '折扣后金额',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
precision: 2,
disabled: true,

View File

@@ -65,6 +65,7 @@ export function useFormSchema(confType: LimitConfType): VbenFormSchema[] {
: '锁定客户数上限',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: `请输入${
LimitConfType.CUSTOMER_QUANTITY_LIMIT === confType
? '拥有客户数上限'

View File

@@ -16,6 +16,7 @@ export const schema: VbenFormSchema[] = [
component: 'InputNumber',
fieldName: 'contactExpireDays',
componentProps: {
class: '!w-full',
min: 0,
precision: 0,
},
@@ -35,6 +36,7 @@ export const schema: VbenFormSchema[] = [
addonAfter: () => '天未成交',
}),
componentProps: {
class: '!w-full',
min: 0,
precision: 0,
},
@@ -63,6 +65,7 @@ export const schema: VbenFormSchema[] = [
component: 'InputNumber',
fieldName: 'notifyDays',
componentProps: {
class: '!w-full',
min: 0,
precision: 0,
},

View File

@@ -92,6 +92,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '价格(元)',
rules: 'required',
componentProps: {
class: '!w-full',
min: 0,
precision: 2,
step: 0.1,

View File

@@ -141,6 +141,7 @@ export function useFormSchema(): VbenFormSchema[] {
component: 'InputNumber',
rules: 'required',
componentProps: {
class: '!w-full',
placeholder: '请输入回款金额',
min: 0,
precision: 2,

View File

@@ -96,6 +96,7 @@ export function useFormSchema(): VbenFormSchema[] {
component: 'InputNumber',
rules: 'required',
componentProps: {
class: '!w-full',
placeholder: '请输入计划回款金额',
min: 0,
precision: 2,
@@ -119,6 +120,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '提前几天提醒',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入提前几天提醒',
min: 0,
},

View File

@@ -44,6 +44,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '排序',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入排序',
precision: 0,
},

View File

@@ -129,6 +129,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '合计付款',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '合计付款',
precision: 2,
formatter: erpPriceInputFormatter,
@@ -140,6 +141,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '优惠金额',
component: 'InputNumber',
componentProps: {
class: '!w-full',
disabled: formType === 'detail',
placeholder: '请输入优惠金额',
precision: 2,
@@ -151,6 +153,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '实际付款',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '实际付款',
precision: 2,
formatter: erpPriceInputFormatter,

View File

@@ -129,6 +129,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '合计收款',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '合计收款',
precision: 2,
formatter: erpPriceInputFormatter,
@@ -140,6 +141,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '优惠金额',
component: 'InputNumber',
componentProps: {
class: '!w-full',
disabled: formType === 'detail',
placeholder: '请输入优惠金额',
precision: 2,
@@ -151,6 +153,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '实际收款',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '实际收款',
precision: 2,
formatter: erpPriceInputFormatter,

View File

@@ -65,6 +65,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '显示顺序',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
placeholder: '请输入显示顺序',
},

View File

@@ -92,6 +92,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '保质期天数',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入保质期天数',
},
},
@@ -100,6 +101,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '重量kg',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入重量kg',
},
},
@@ -108,6 +110,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '采购价格',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入采购价格,单位:元',
precision: 2,
min: 0,
@@ -119,6 +122,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '销售价格',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入销售价格,单位:元',
precision: 2,
min: 0,
@@ -130,6 +134,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '最低价格',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入最低价格,单位:元',
precision: 2,
min: 0,

View File

@@ -117,6 +117,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '优惠率(%)',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入优惠率',
min: 0,
max: 100,
@@ -129,6 +130,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '付款优惠',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '付款优惠',
precision: 2,
formatter: erpPriceInputFormatter,
@@ -140,6 +142,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '优惠后金额',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '优惠后金额',
precision: 2,
formatter: erpPriceInputFormatter,
@@ -160,6 +163,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '其他费用',
component: 'InputNumber',
componentProps: {
class: '!w-full',
disabled: formType === 'detail',
placeholder: '请输入其他费用',
precision: 2,
@@ -184,6 +188,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '应付金额',
component: 'InputNumber',
componentProps: {
class: '!w-full',
precision: 2,
min: 0,
disabled: true,

View File

@@ -103,6 +103,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '优惠率(%)',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入优惠率',
min: 0,
max: 100,
@@ -115,6 +116,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '付款优惠',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '付款优惠',
precision: 2,
formatter: erpPriceInputFormatter,
@@ -126,6 +128,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '优惠后金额',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '优惠后金额',
precision: 2,
formatter: erpPriceInputFormatter,
@@ -148,6 +151,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
{
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入支付订金',
precision: 2,
min: 0,

View File

@@ -117,6 +117,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '优惠率(%)',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入优惠率',
min: 0,
max: 100,
@@ -129,6 +130,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '退款优惠',
component: 'InputNumber',
componentProps: {
class: '!w-full',
precision: 2,
formatter: erpPriceInputFormatter,
disabled: true,
@@ -139,6 +141,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '优惠后金额',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '优惠后金额',
precision: 2,
formatter: erpPriceInputFormatter,
@@ -159,6 +162,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '其他费用',
component: 'InputNumber',
componentProps: {
class: '!w-full',
disabled: formType === 'detail',
placeholder: '请输入其他费用',
precision: 2,
@@ -183,6 +187,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '应退金额',
component: 'InputNumber',
componentProps: {
class: '!w-full',
precision: 2,
min: 0,
disabled: true,

View File

@@ -82,6 +82,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '排序',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入排序',
},
rules: 'required',
@@ -99,6 +100,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '税率(%)',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入税率',
min: 0,
precision: 2,

View File

@@ -82,6 +82,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '排序',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入排序',
precision: 0,
},
@@ -100,6 +101,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '税率(%)',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入税率',
precision: 2,
},

View File

@@ -116,6 +116,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '优惠率(%)',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入优惠率',
min: 0,
max: 100,
@@ -128,6 +129,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '付款优惠',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '收款优惠',
precision: 2,
formatter: erpPriceInputFormatter,
@@ -139,6 +141,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '优惠后金额',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '优惠后金额',
precision: 2,
formatter: erpPriceInputFormatter,
@@ -161,6 +164,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
{
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入收取订金',
precision: 2,
min: 0,

View File

@@ -134,6 +134,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '优惠率(%)',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入优惠率',
min: 0,
max: 100,
@@ -146,6 +147,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '收款优惠',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '付款优惠',
precision: 2,
formatter: erpPriceInputFormatter,
@@ -157,6 +159,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '优惠后金额',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '优惠后金额',
precision: 2,
formatter: erpPriceInputFormatter,
@@ -177,6 +180,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '其他费用',
component: 'InputNumber',
componentProps: {
class: '!w-full',
disabled: formType === 'detail',
placeholder: '请输入其他费用',
precision: 2,
@@ -204,6 +208,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '应收金额',
component: 'InputNumber',
componentProps: {
class: '!w-full',
precision: 2,
min: 0,
disabled: true,

View File

@@ -130,6 +130,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '优惠率(%)',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入优惠率',
min: 0,
max: 100,
@@ -142,6 +143,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '退款优惠',
component: 'InputNumber',
componentProps: {
class: '!w-full',
precision: 2,
formatter: erpPriceInputFormatter,
disabled: true,
@@ -152,6 +154,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '优惠后金额',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '优惠后金额',
precision: 2,
formatter: erpPriceInputFormatter,
@@ -172,6 +175,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '其他费用',
component: 'InputNumber',
componentProps: {
class: '!w-full',
disabled: formType === 'detail',
placeholder: '请输入其他费用',
precision: 2,
@@ -197,6 +201,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '应收金额',
component: 'InputNumber',
componentProps: {
class: '!w-full',
precision: 2,
min: 0,
disabled: true,

View File

@@ -51,6 +51,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '仓储费(元)',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入仓储费,单位:元/天/KG',
min: 0,
precision: 2,
@@ -61,6 +62,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '搬运费(元)',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入搬运费,单位:元',
min: 0,
precision: 2,
@@ -79,6 +81,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '排序',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入排序',
precision: 0,
},

View File

@@ -82,6 +82,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '主机端口',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
placeholder: '请输入主机端口',
},

View File

@@ -68,6 +68,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '重试次数',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入重试次数。设置为 0 时,不进行重试',
min: 0,
},
@@ -78,6 +79,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '重试间隔',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入重试间隔,单位:毫秒。设置为 0 时,无需间隔',
min: 0,
},
@@ -88,6 +90,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '监控超时时间',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入监控超时时间,单位:毫秒',
min: 0,
},

View File

@@ -122,8 +122,8 @@ export function useAdvancedFormSchema(): VbenFormSchema[] {
label: '设备经度',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入设备经度',
class: 'w-full',
min: -180,
max: 180,
precision: 6,
@@ -140,8 +140,8 @@ export function useAdvancedFormSchema(): VbenFormSchema[] {
label: '设备纬度',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入设备纬度',
class: 'w-full',
min: -90,
max: 90,
precision: 6,

View File

@@ -62,7 +62,7 @@ function openEditForm(row: IotDeviceApi.Device) {
<div>
<h2 class="text-xl font-bold">{{ device.deviceName }}</h2>
</div>
<div class="space-x-2">
<div class="flex gap-2">
<Button
v-if="product.status === 0"
v-access:code="['iot:device:update']"

View File

@@ -70,6 +70,7 @@ const [Form, formApi] = useVbenForm({
label: '端口',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入端口',
min: 1,
max: 65_535,
@@ -86,6 +87,7 @@ const [Form, formApi] = useVbenForm({
label: '从站地址',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入从站地址,范围 1-247',
min: 1,
max: 247,
@@ -98,6 +100,7 @@ const [Form, formApi] = useVbenForm({
label: '连接超时(ms)',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入连接超时时间',
min: 1000,
step: 1000,
@@ -114,6 +117,7 @@ const [Form, formApi] = useVbenForm({
label: '重试间隔(ms)',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入重试间隔',
min: 1000,
step: 1000,

View File

@@ -111,6 +111,7 @@ function useFormSchema(): VbenFormSchema[] {
label: '寄存器地址',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入寄存器地址',
min: 0,
max: 65_535,
@@ -133,6 +134,7 @@ function useFormSchema(): VbenFormSchema[] {
label: '寄存器数量',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入寄存器数量',
min: 1,
max: 125,
@@ -177,6 +179,7 @@ function useFormSchema(): VbenFormSchema[] {
label: '缩放因子',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入缩放因子',
precision: 6,
step: 0.1,
@@ -188,6 +191,7 @@ function useFormSchema(): VbenFormSchema[] {
label: '轮询间隔(ms)',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入轮询间隔',
min: 100,
step: 1000,

View File

@@ -284,7 +284,7 @@ onMounted(async () => {
<DeviceImportFormModal @success="handleRefresh" />
<!-- 统一搜索工具栏 -->
<Card :body-style="{ padding: '16px' }" class="mb-4">
<Card :body-style="{ padding: '16px' }" class="!mb-2">
<!-- 搜索表单 -->
<div class="mb-3 flex flex-wrap items-center gap-3">
<Select

View File

@@ -35,8 +35,8 @@ export function useFormSchema(): VbenFormSchema[] {
label: '分类排序',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入分类排序',
class: 'w-full',
min: 0,
precision: 0,
},

View File

@@ -147,19 +147,6 @@ export function useBasicFormSchema(
help: 'iot-gateway-server 默认根据接入的协议类型确定数据格式,仅 MQTT、EMQX 协议支持自定义序列化类型',
rules: 'required',
},
// TODO @haohao这个貌似不需要
{
fieldName: 'status',
label: '产品状态',
component: 'RadioGroup',
componentProps: {
options: getDictOptions(DICT_TYPE.IOT_PRODUCT_STATUS, 'number'),
buttonStyle: 'solid',
optionType: 'button',
},
defaultValue: 0,
rules: 'required',
},
];
}
@@ -248,15 +235,6 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
name: 'CellImage',
},
},
{
field: 'status',
title: '产品状态',
minWidth: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.IOT_PRODUCT_STATUS },
},
},
{
field: 'createTime',
title: '创建时间',

View File

@@ -11,6 +11,7 @@ import { message, Tabs } from 'ant-design-vue';
import { getDeviceCount } from '#/api/iot/device/device';
import { getProduct } from '#/api/iot/product/product';
import IoTProductThingModel from '#/views/iot/thingmodel/index.vue';
import { IOT_PROVIDE_KEY } from '#/views/iot/utils/constants';
import ProductDetailsHeader from './modules/header.vue';
import ProductDetailsInfo from './modules/info.vue';
@@ -25,7 +26,8 @@ const loading = ref(true);
const product = ref<IotProductApi.Product>({} as IotProductApi.Product);
const activeTab = ref('info');
provide('product', product); // 提供产品信息给子组件
/** 向子组件提供产品信息 */
provide(IOT_PROVIDE_KEY.PRODUCT, product);
/** 获取产品详情 */
async function getProductData(productId: number) {
@@ -82,10 +84,7 @@ onMounted(async () => {
<ProductDetailsInfo v-if="activeTab === 'info'" :product="product" />
</Tabs.TabPane>
<Tabs.TabPane key="thingModel" tab="物模型(功能定义)">
<IoTProductThingModel
v-if="activeTab === 'thingModel'"
:product-id="id"
/>
<IoTProductThingModel v-if="activeTab === 'thingModel'" />
</Tabs.TabPane>
</Tabs>
</Page>

View File

@@ -90,7 +90,7 @@ function handleUnpublish(product: IotProductApi.Product) {
<div>
<h2 class="text-xl font-bold">{{ product.name }}</h2>
</div>
<div class="space-x-2">
<div class="flex gap-2">
<Button
:disabled="product.status === ProductStatusEnum.PUBLISHED"
@click="openEditForm(product)"

View File

@@ -175,7 +175,7 @@ onMounted(() => {
<FormModal @success="handleRefresh" />
<!-- 统一搜索工具栏 -->
<Card :body-style="{ padding: '16px' }" class="mb-4">
<Card :body-style="{ padding: '16px' }" class="!mb-2">
<!-- 搜索表单 -->
<div class="mb-3 flex items-center gap-3">
<Input

View File

@@ -115,11 +115,6 @@ onMounted(() => {
<div class="ml-3 min-w-0 flex-1">
<div class="product-title">{{ item.name }}</div>
</div>
<DictTag
:type="DICT_TYPE.IOT_PRODUCT_STATUS"
:value="item.status"
class="status-tag"
/>
</div>
<!-- 内容区域 -->
<div class="mb-3 flex items-start">
@@ -269,11 +264,6 @@ onMounted(() => {
white-space: nowrap;
}
// 状态标签
.status-tag {
font-size: 12px;
}
// 信息列表
.info-list {
.info-item {

View File

@@ -4,6 +4,8 @@ import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { getDataTypeOptionsLabel } from '#/views/iot/utils/constants';
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
@@ -27,7 +29,7 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
{
field: 'type',
title: '功能类型',
minWidth: 20,
minWidth: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.IOT_THING_MODEL_TYPE },
@@ -41,17 +43,16 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
{
field: 'identifier',
title: '标识符',
minWidth: 20,
minWidth: 120,
},
{
field: 'dataType',
title: '数据类型',
minWidth: 50,
slots: { default: 'dataType' },
minWidth: 100,
formatter: ({ row }) =>
getDataTypeOptionsLabel(row.property?.dataType) || '-',
},
{
field: 'property',
title: '属性',
title: '数据定义',
minWidth: 200,
slots: { default: 'dataDefinition' },
},

View File

@@ -1,99 +1,90 @@
<script setup lang="ts">
import type { Ref } from 'vue';
import type { IotProductApi } from '#/api/iot/product/product';
import type { ThingModelData } from '#/api/iot/thingmodel';
import { onMounted, provide, ref } from 'vue';
import { computed, inject } from 'vue';
import { Page } from '@vben/common-ui';
import { Page, useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getProduct } from '#/api/iot/product/product';
import { deleteThingModel, getThingModelPage } from '#/api/iot/thingmodel';
import { $t } from '#/locales';
import { IOT_PROVIDE_KEY } from '#/views/iot/utils/constants';
import { getDataTypeOptionsLabel, IOT_PROVIDE_KEY } from '../utils/constants';
import { useGridColumns, useGridFormSchema } from './data';
import { DataDefinition } from './modules/components';
import ThingModelForm from './modules/thing-model-form.vue';
import ThingModelTsl from './modules/thing-model-tsl.vue';
import Form from './modules/form.vue';
import Tsl from './modules/tsl.vue';
defineOptions({ name: 'IoTThingModel' });
const props = defineProps<{
productId: number;
}>();
const product = inject<Ref<IotProductApi.Product>>(IOT_PROVIDE_KEY.PRODUCT);
const productId = computed(() => product?.value?.id);
const product = ref<IotProductApi.Product>({} as IotProductApi.Product); // 产品信息
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
provide(IOT_PROVIDE_KEY.PRODUCT, product); // 提供产品信息给子组件
const [TslModal, tslModalApi] = useVbenModal({
connectedComponent: Tsl,
destroyOnClose: true,
});
// TODO @haohaoform 是不是用 web-antd/src/views/system/user/index.vue 里 open 的风格;
const thingModelFormRef = ref();
// TODO @haohaothingModelTSLRef 应该是个 modal也可以调整下风格
const thingModelTSLRef = ref();
// TODO @haohao方法的顺序、注释、调整的和别的模块一致。
// 新增功能
function handleCreate() {
thingModelFormRef.value?.open('create');
}
// 编辑功能
function handleEdit(row: any) {
thingModelFormRef.value?.open('update', row.id);
}
// 删除功能
async function handleDelete(row: any) {
// TODO @haohao应该有个 loading类似别的模块写法
try {
await deleteThingModel(row.id);
message.success('删除成功');
gridApi.reload();
} catch (error) {
console.error('删除失败:', error);
}
}
// 打开 TSL
function handleOpenTSL() {
thingModelTSLRef.value?.open();
}
// 获取数据类型标签
// TODO @haohao可以直接在 data.ts 就写掉这个逻辑;
function getDataTypeLabel(row: any) {
return getDataTypeOptionsLabel(row.property?.dataType) || '-';
}
// 刷新表格
/** 刷新表格 */
function handleRefresh() {
gridApi.reload();
gridApi.query();
}
// 获取产品信息
async function getProductData() {
/** 新增物模型 */
function handleCreate() {
formModalApi.setData(null).open();
}
/** 编辑物模型 */
function handleEdit(row: ThingModelData) {
formModalApi.setData({ id: row.id }).open();
}
/** 删除物模型 */
async function handleDelete(row: ThingModelData) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
});
try {
product.value = await getProduct(props.productId);
} catch (error) {
console.error('获取产品信息失败:', error);
await deleteThingModel(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
handleRefresh();
} finally {
hideLoading();
}
}
// TODO @haohao字段的顺序调整成别的模块一直
/** 打开 TSL 弹窗 */
function handleOpenTsl() {
tslModalApi.open();
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }: any, formValues: any) => {
query: async ({ page }, formValues) => {
return await getThingModelPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
productId: props.productId,
productId: productId.value,
...formValues,
});
},
@@ -108,64 +99,55 @@ const [Grid, gridApi] = useVbenVxeGrid({
search: true,
},
},
formOptions: {
schema: useGridFormSchema(),
},
});
// 初始化
onMounted(async () => {
await getProductData();
});
</script>
<template>
<Page auto-content-height>
<Grid>
<FormModal @success="handleRefresh" />
<TslModal />
<Grid table-title="物模型列表">
<template #toolbar-tools>
<TableAction
:actions="[
{
label: '添加功能',
label: $t('ui.actionTitle.create', ['物模型']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['iot:thing-model:create'],
onClick: handleCreate,
},
{
label: 'TSL',
type: 'default',
color: 'success', // TODO @haohao貌似 color 可以去掉应该是不生效的哈。ps另外也给搞个 icon
onClick: handleOpenTSL,
type: 'primary',
auth: ['iot:thing-model:query'],
onClick: handleOpenTsl,
},
]"
/>
</template>
<!-- 数据类型列 -->
<template #dataType="{ row }">
<span>{{ getDataTypeLabel(row) }}</span>
</template>
<!-- 数据定义列 -->
<!-- TODO @haohao可以在 data.ts 就写掉这个逻辑 -->
<template #dataDefinition="{ row }">
<DataDefinition :data="row" />
</template>
<!-- 操作列 -->
<template #actions="{ row }">
<TableAction
:actions="[
{
label: '编辑',
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
auth: ['iot:thing-model:update'],
onClick: handleEdit.bind(null, row),
},
{
label: '删除',
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['iot:thing-model:delete'],
popConfirm: {
title: '确认删除该功能吗?',
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: handleDelete.bind(null, row),
},
},
@@ -173,10 +155,5 @@ onMounted(async () => {
/>
</template>
</Grid>
<!-- 物模型表单 -->
<ThingModelForm ref="thingModelFormRef" @success="handleRefresh" />
<!-- TSL 弹窗 -->
<ThingModelTsl ref="thingModelTSLRef" />
</Page>
</template>

View File

@@ -1,4 +1,3 @@
<!-- TODO @haohao如果是模块内用的就用 modules 等后面点在看优先级 -->
<script lang="ts" setup>
import type { ThingModelData } from '#/api/iot/thingmodel';
@@ -13,83 +12,63 @@ import {
IoTThingModelTypeEnum,
} from '#/views/iot/utils/constants';
/** 数据定义展示组件 */
defineOptions({ name: 'DataDefinition' });
const NUMBER_TYPES = new Set<string>([
IoTDataSpecsDataTypeEnum.INT,
IoTDataSpecsDataTypeEnum.DOUBLE,
IoTDataSpecsDataTypeEnum.FLOAT,
]);
const PLACEHOLDER_TYPES = new Set<string>([
IoTDataSpecsDataTypeEnum.ARRAY,
IoTDataSpecsDataTypeEnum.STRUCT,
IoTDataSpecsDataTypeEnum.DATE,
]);
const LIST_TYPES = new Set<string>([
IoTDataSpecsDataTypeEnum.BOOL,
IoTDataSpecsDataTypeEnum.ENUM,
]);
const props = defineProps<{ data: ThingModelData }>();
const formattedDataSpecsList = computed(() => {
if (
!props.data.property?.dataSpecsList ||
props.data.property.dataSpecsList.length === 0
) {
if (!props.data.property?.dataSpecsList?.length) {
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
) {
const list = props.data.property?.dataSpecsList;
if (!list?.length) {
return '-';
}
const first = props.data.property.dataSpecsList[0];
const count = props.data.property.dataSpecsList.length;
return count > 1
? `${first.value}-${first.name}${count}`
const first = list[0];
return list.length > 1
? `${first.value}-${first.name}${list.length}`
: `${first.value}-${first.name}`;
}); // 显示的简短文本(第一个值)
});
</script>
<template>
<!-- 属性 -->
<template v-if="Number(data.type) === IoTThingModelTypeEnum.PROPERTY">
<!-- 非列表型数值 -->
<div
v-if="
[
IoTDataSpecsDataTypeEnum.INT,
IoTDataSpecsDataTypeEnum.DOUBLE,
IoTDataSpecsDataTypeEnum.FLOAT,
].includes(data.property?.dataType as any)
"
>
<template v-if="data.type === IoTThingModelTypeEnum.PROPERTY">
<div v-if="NUMBER_TYPES.has(data.property?.dataType as any)">
取值范围{{
`${data.property?.dataSpecs?.min}~${data.property?.dataSpecs?.max}`
}}
</div>
<!-- 非列表型:文本 -->
<div v-if="IoTDataSpecsDataTypeEnum.TEXT === data.property?.dataType">
<div v-if="data.property?.dataType === IoTDataSpecsDataTypeEnum.TEXT">
数据长度{{ data.property?.dataSpecs?.length }}
</div>
<!-- 列表型: 数组、结构、时间(特殊) -->
<div
v-if="
[
IoTDataSpecsDataTypeEnum.ARRAY,
IoTDataSpecsDataTypeEnum.STRUCT,
IoTDataSpecsDataTypeEnum.DATE,
].includes(data.property?.dataType as any)
"
>
-
</div>
<!-- 列表型: 布尔值、枚举 -->
<div
v-if="
[IoTDataSpecsDataTypeEnum.BOOL, IoTDataSpecsDataTypeEnum.ENUM].includes(
data.property?.dataType as any,
)
"
>
<div v-if="PLACEHOLDER_TYPES.has(data.property?.dataType as any)">-</div>
<div v-if="LIST_TYPES.has(data.property?.dataType as any)">
<Tooltip :title="formattedDataSpecsList" placement="topLeft">
<span class="data-specs-text">
<span
class="cursor-help border-b border-dashed border-gray-300 hover:border-blue-500 hover:text-blue-500"
>
{{
IoTDataSpecsDataTypeEnum.BOOL === data.property?.dataType
data.property?.dataType === IoTDataSpecsDataTypeEnum.BOOL
? '布尔值'
: '枚举值'
}}{{ shortText }}
@@ -98,25 +77,12 @@ const shortText = computed(() => {
</div>
</template>
<!-- 服务 -->
<div v-if="Number(data.type) === IoTThingModelTypeEnum.SERVICE">
<div v-if="data.type === IoTThingModelTypeEnum.SERVICE">
调用方式
{{ getThingModelServiceCallTypeLabel(data.service?.callType as any) }}
</div>
<!-- 事件 -->
<div v-if="Number(data.type) === IoTThingModelTypeEnum.EVENT">
<div v-if="data.type === IoTThingModelTypeEnum.EVENT">
事件类型{{ getEventTypeLabel(data.event?.type as any) }}
</div>
</template>
<style lang="scss" scoped>
/** TODO @haohaotindwind */
.data-specs-text {
cursor: help;
border-bottom: 1px dashed #d9d9d9;
&:hover {
color: #1890ff;
border-bottom-color: #1890ff;
}
}
</style>

View File

@@ -5,21 +5,29 @@ import type { Ref } from 'vue';
import { useVModel } from '@vueuse/core';
import { Form, Input, Radio } from 'ant-design-vue';
import { ThingModelFormRules } from '#/api/iot/thingmodel';
import {
getDataTypeOptions,
IoTDataSpecsDataTypeEnum,
} from '#/views/iot/utils/constants';
import ThingModelStructDataSpecs from './thing-model-struct-data-specs.vue';
import ThingModelStructDataSpecs from './struct.vue';
/** 数组型的 dataSpecs 配置组件 */
defineOptions({ name: 'ThingModelArrayDataSpecs' });
/** 数组元素禁止选择的类型 */
const EXCLUDED_CHILD_TYPES = new Set<string>([
IoTDataSpecsDataTypeEnum.ENUM,
IoTDataSpecsDataTypeEnum.ARRAY,
IoTDataSpecsDataTypeEnum.DATE,
]);
const childDataTypeOptions = getDataTypeOptions().filter(
(item) => !EXCLUDED_CHILD_TYPES.has(item.value),
);
const props = defineProps<{ modelValue: any }>();
const emits = defineEmits(['update:modelValue']);
const dataSpecs = useVModel(props, 'modelValue', emits) as Ref<any>;
/** 元素类型改变时间。当值为 struct 时,对 dataSpecs 中的 dataSpecsList 进行初始化 */
/** 元素类型切到 struct 时,初始化 dataSpecsList 占位 */
function handleChange(val: any) {
if (val !== IoTDataSpecsDataTypeEnum.STRUCT) {
return;
@@ -31,29 +39,25 @@ function handleChange(val: any) {
<template>
<Form.Item
:name="['property', 'dataSpecs', 'childDataType']"
:rules="ThingModelFormRules.childDataType"
label="元素类型"
>
<Radio.Group v-model:value="dataSpecs.childDataType" @change="handleChange">
<template v-for="item in getDataTypeOptions()" :key="item.value">
<Radio
v-if="
!(
[
IoTDataSpecsDataTypeEnum.ENUM,
IoTDataSpecsDataTypeEnum.ARRAY,
IoTDataSpecsDataTypeEnum.DATE,
] as any[]
).includes(item.value)
"
:value="item.value"
class="w-1/3"
>
{{ `${item.value}(${item.label})` }}
</Radio>
</template>
<Radio
v-for="item in childDataTypeOptions"
:key="item.value"
:value="item.value"
class="w-1/3"
>
{{ `${item.value}(${item.label})` }}
</Radio>
</Radio.Group>
</Form.Item>
<Form.Item :name="['property', 'dataSpecs', 'size']" label="元素个数">
<Form.Item
:name="['property', 'dataSpecs', 'size']"
:rules="ThingModelFormRules.size"
label="元素个数"
>
<Input
v-model:value="dataSpecs.size"
placeholder="请输入数组中的元素个数"

View File

@@ -0,0 +1,131 @@
<!-- dataTypeenum 数组类型 -->
<script lang="ts" setup>
import type { Ref } from 'vue';
import { isEmpty } from '@vben/utils';
import { useVModel } from '@vueuse/core';
import { Button, Form, Input, message } from 'ant-design-vue';
import { buildIdentifierLikeNameValidator } from '#/api/iot/thingmodel';
const props = defineProps<{ modelValue: any }>();
const emits = defineEmits(['update:modelValue']);
const dataSpecsList = useVModel(props, 'modelValue', emits) as Ref<any[]>;
const validateEnumName = buildIdentifierLikeNameValidator('枚举描述');
/** 添加枚举项 */
function addEnum() {
dataSpecsList.value.push({ name: '', value: '' } as any);
}
/** 删除枚举项 */
function deleteEnum(index: number) {
if (dataSpecsList.value.length === 1) {
message.warning('至少需要一个枚举项');
return;
}
dataSpecsList.value.splice(index, 1);
}
/** 校验单项枚举值:必填、数字、不重复 */
function validateEnumValue(_rule: any, value: any, callback: any) {
if (isEmpty(value)) {
callback(new Error('枚举值不能为空'));
return;
}
if (Number.isNaN(Number(value))) {
callback(new Error('枚举值必须是数字'));
return;
}
const sameCount = dataSpecsList.value.filter((it) => it.value === value)
.length;
if (sameCount > 1) {
callback(new Error('枚举值不能重复'));
return;
}
callback();
}
/** 校验整个枚举列表:非空、无空项、无非法数字、无重复 */
function validateEnumList(_rule: any, _value: any, callback: any) {
if (isEmpty(dataSpecsList.value)) {
callback(new Error('请至少添加一个枚举项'));
return;
}
const hasEmpty = dataSpecsList.value.some(
(item) => isEmpty(item.value) || isEmpty(item.name),
);
if (hasEmpty) {
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);
if (new Set(values).size !== values.length) {
callback(new Error('存在重复的枚举值'));
return;
}
callback();
}
</script>
<template>
<Form.Item
:rules="[{ validator: validateEnumList, trigger: 'change' }]"
label="枚举项"
>
<div class="flex flex-col">
<div class="flex items-center">
<span class="flex-1"> 参数值 </span>
<span class="flex-1"> 参数描述 </span>
</div>
<div
v-for="(item, index) in dataSpecsList"
:key="index"
class="mb-[5px] flex items-center justify-between"
>
<Form.Item
:name="['property', 'dataSpecsList', index, 'value']"
:rules="[
{ required: true, message: '枚举值不能为空', trigger: 'blur' },
{ validator: validateEnumValue, trigger: 'blur' },
]"
class="mb-0 flex-1"
>
<Input v-model:value="item.value" placeholder="请输入枚举值如「0」" />
</Form.Item>
<span class="mx-2">~</span>
<Form.Item
:name="['property', 'dataSpecsList', index, 'name']"
:rules="[
{ required: true, message: '枚举描述不能为空', trigger: 'blur' },
{ validator: validateEnumName, trigger: 'blur' },
]"
class="mb-0 flex-1"
>
<Input v-model:value="item.name" placeholder="对该枚举项的描述" />
</Form.Item>
<Button class="ml-2.5" type="link" @click="deleteEnum(index)">
删除
</Button>
</div>
<Button type="link" @click="addEnum">+ 添加枚举项</Button>
</div>
</Form.Item>
</template>
<style lang="scss" scoped>
:deep(.ant-form-item) {
.ant-form-item {
margin-bottom: 0;
}
}
</style>

View File

@@ -0,0 +1,4 @@
export { default as ThingModelArrayDataSpecs } from './array.vue';
export { default as ThingModelEnumDataSpecs } from './enum.vue';
export { default as ThingModelNumberDataSpecs } from './number.vue';
export { default as ThingModelStructDataSpecs } from './struct.vue';

View File

@@ -10,9 +10,6 @@ import { getDictOptions } from '@vben/hooks';
import { useVModel } from '@vueuse/core';
import { Form, Input, Select } from 'ant-design-vue';
/** 数值型的 dataSpecs 配置组件 */
defineOptions({ name: 'ThingModelNumberDataSpecs' });
const props = defineProps<{ modelValue: any }>();
const emits = defineEmits(['update:modelValue']);
const dataSpecs = useVModel(
@@ -21,13 +18,15 @@ const dataSpecs = useVModel(
emits,
) as Ref<DataSpecsNumberData>;
/** 单位发生变化时触发 */
const unitChange = (UnitSpecs: any) => {
if (!UnitSpecs) return;
const [unitName, unit] = String(UnitSpecs).split('-');
/** 单位下拉变化时,拆出 unitName 与 unit 回写 */
function unitChange(unitSpecs: any) {
if (!unitSpecs) {
return;
}
const [unitName, unit] = String(unitSpecs).split('-');
dataSpecs.value.unitName = unitName;
dataSpecs.value.unit = unit;
};
}
</script>
<template>
@@ -52,7 +51,7 @@ const unitChange = (UnitSpecs: any) => {
"
show-search
placeholder="请选择单位"
class="w-1/1"
class="w-full"
@change="unitChange"
>
<Select.Option

View File

@@ -0,0 +1,157 @@
<script lang="ts" setup>
import type { Ref } from 'vue';
import { onMounted, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { isEmpty } from '@vben/utils';
import { useVModel } from '@vueuse/core';
import { Button, Divider, Form, Input } from 'ant-design-vue';
import { ThingModelFormRules } from '#/api/iot/thingmodel';
import { IoTDataSpecsDataTypeEnum } from '#/views/iot/utils/constants';
import ThingModelProperty from '../property.vue';
const props = defineProps<{ modelValue: any }>();
const emits = defineEmits(['update:modelValue']);
const dataSpecsList = useVModel(props, 'modelValue', emits) as Ref<any[]>;
const structFormRef = ref();
const formData = ref<any>(buildEmptyFormData());
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
try {
await structFormRef.value?.validate();
} catch {
return;
}
const data = formData.value;
const item = {
identifier: data.identifier,
name: data.name,
description: data.description,
dataType: IoTDataSpecsDataTypeEnum.STRUCT,
childDataType: data.property.dataType,
dataSpecs:
!isEmpty(data.property.dataSpecs) &&
Object.keys(data.property.dataSpecs).length > 1
? data.property.dataSpecs
: undefined,
dataSpecsList: isEmpty(data.property.dataSpecsList)
? undefined
: data.property.dataSpecsList,
};
const existingIndex = dataSpecsList.value.findIndex(
(spec) => spec.identifier === data.identifier,
);
if (existingIndex === -1) {
dataSpecsList.value.push(item);
} else {
dataSpecsList.value[existingIndex] = item;
}
await modalApi.close();
},
onOpenChange(isOpen: boolean) {
if (!isOpen) {
return;
}
formData.value = buildEmptyFormData();
structFormRef.value?.clearValidate?.();
const data = modalApi.getData<any>();
if (isEmpty(data)) {
return;
}
formData.value = {
identifier: data.identifier ?? '',
name: data.name ?? '',
description: data.description ?? '',
property: {
dataType: data.childDataType ?? IoTDataSpecsDataTypeEnum.INT,
dataSpecs: data.dataSpecs ?? {},
dataSpecsList: data.dataSpecsList ?? [],
},
};
},
});
/** 构造空白结构体表单 */
function buildEmptyFormData() {
return {
identifier: '',
name: '',
description: '',
property: {
dataType: IoTDataSpecsDataTypeEnum.INT,
dataSpecs: { dataType: IoTDataSpecsDataTypeEnum.INT },
dataSpecsList: [],
},
};
}
/** 打开结构体表单 */
function openStructForm(val: any) {
modalApi.setData(val).open();
}
/** 删除结构体项 */
function deleteStructItem(index: number) {
dataSpecsList.value.splice(index, 1);
}
onMounted(() => {
if (isEmpty(dataSpecsList.value)) {
dataSpecsList.value = [];
}
});
</script>
<template>
<Form.Item label="属性对象">
<div
v-for="(item, index) in dataSpecsList"
:key="index"
class="mb-2.5 flex w-full justify-between bg-gray-100 px-2.5 dark:bg-gray-800"
>
<span>参数{{ item.name }}</span>
<div>
<Button type="link" @click="openStructForm(item)">编辑</Button>
<Divider type="vertical" />
<Button danger type="link" @click="deleteStructItem(index)">
删除
</Button>
</div>
</div>
<Button type="link" @click="openStructForm(null)">+ 新增参数</Button>
</Form.Item>
<!-- 结构体参数表单 -->
<Modal class="w-2/5" title="结构体参数">
<Form
ref="structFormRef"
:label-col="{ span: 6 }"
:model="formData"
:wrapper-col="{ span: 18 }"
class="mx-4"
>
<Form.Item
:rules="ThingModelFormRules.name"
label="参数名称"
name="name"
>
<Input v-model:value="formData.name" placeholder="请输入参数名称" />
</Form.Item>
<Form.Item
:rules="ThingModelFormRules.identifier"
label="标识符"
name="identifier"
>
<Input v-model:value="formData.identifier" placeholder="请输入标识符" />
</Form.Item>
<!-- 属性配置 -->
<ThingModelProperty v-model="formData.property" is-struct-data-specs />
</Form>
</Modal>
</template>

View File

@@ -1,4 +0,0 @@
export { default as ThingModelArrayDataSpecs } from './thing-model-array-data-specs.vue';
export { default as ThingModelEnumDataSpecs } from './thing-model-enum-data-specs.vue';
export { default as ThingModelNumberDataSpecs } from './thing-model-number-data-specs.vue';
export { default as ThingModelStructDataSpecs } from './thing-model-struct-data-specs.vue';

View File

@@ -1,67 +0,0 @@
<!-- dataTypeenum 数组类型 -->
<script lang="ts" setup>
import type { Ref } from 'vue';
import { useVModel } from '@vueuse/core';
import { Button, Form, Input, message } from 'ant-design-vue';
/** 枚举型的 dataSpecs 配置组件 */
defineOptions({ name: 'ThingModelEnumDataSpecs' });
const props = defineProps<{ modelValue: any }>();
const emits = defineEmits(['update:modelValue']);
const dataSpecsList = useVModel(props, 'modelValue', emits) as Ref<any[]>;
/** 添加枚举项 */
function addEnum() {
dataSpecsList.value.push({
name: '', // 枚举项的名称
value: '', // 枚举值
} as any);
}
/** 删除枚举项 */
function deleteEnum(index: number) {
if (dataSpecsList.value.length === 1) {
message.warning('至少需要一个枚举项');
return;
}
dataSpecsList.value.splice(index, 1);
}
</script>
<template>
<Form.Item label="枚举项">
<div class="flex flex-col">
<div class="flex items-center">
<span class="flex-1"> 参数值 </span>
<span class="flex-1"> 参数描述 </span>
</div>
<div
v-for="(item, index) in dataSpecsList"
:key="index"
class="mb-5px flex items-center justify-between"
>
<div class="flex-1">
<Input v-model:value="item.value" placeholder="请输入枚举值,如'0'" />
</div>
<span class="mx-2">~</span>
<div class="flex-1">
<Input v-model:value="item.name" placeholder="对该枚举项的描述" />
</div>
<Button class="ml-10px" type="link" @click="deleteEnum(index)">
删除
</Button>
</div>
<Button type="link" @click="addEnum">+添加枚举项</Button>
</div>
</Form.Item>
</template>
<style lang="scss" scoped>
:deep(.ant-form-item) {
.ant-form-item {
margin-bottom: 0;
}
}
</style>

View File

@@ -1,169 +0,0 @@
<!-- dataTypestruct 数组类型 -->
<script lang="ts" setup>
import type { Ref } from 'vue';
import { nextTick, onMounted, ref, unref } from 'vue';
import { isEmpty } from '@vben/utils';
import { useVModel } from '@vueuse/core';
import { Button, Divider, Form, Input, Modal } from 'ant-design-vue';
import { IoTDataSpecsDataTypeEnum } from '#/views/iot/utils/constants';
import ThingModelProperty from '../thing-model-property.vue';
/** Struct 型的 dataSpecs 配置组件 */
defineOptions({ name: 'ThingModelStructDataSpecs' });
const props = defineProps<{ modelValue: any }>();
const emits = defineEmits(['update:modelValue']);
const dataSpecsList = useVModel(props, 'modelValue', emits) as Ref<any[]>;
const dialogVisible = ref(false); // 弹窗的是否展示
const dialogTitle = ref('新增参数'); // 弹窗的标题
const formLoading = ref(false); // 表单的加载中1修改时的数据加载2提交的按钮禁用
const structFormRef = ref(); // 表单 ref
const formData = ref<any>({
property: {
dataType: IoTDataSpecsDataTypeEnum.INT,
dataSpecs: {
dataType: IoTDataSpecsDataTypeEnum.INT,
},
dataSpecsList: [],
},
});
/** 打开 struct 表单 */
function openStructForm(val: any) {
dialogVisible.value = true;
resetForm();
if (isEmpty(val)) {
return;
}
// 编辑时回显数据
const valData = val as any;
formData.value = {
identifier: valData?.identifier || '',
name: valData?.name || '',
description: valData?.description || '',
property: {
dataType: valData?.childDataType || IoTDataSpecsDataTypeEnum.INT,
dataSpecs: valData?.dataSpecs ?? {},
dataSpecsList: valData?.dataSpecsList ?? [],
},
};
// 确保 property.dataType 有值
if (!formData.value.property.dataType) {
formData.value.property.dataType = IoTDataSpecsDataTypeEnum.INT;
}
}
/** 删除 struct 项 */
function deleteStructItem(index: number) {
dataSpecsList.value.splice(index, 1);
}
/** 添加参数 */
async function submitForm() {
await structFormRef.value.validate();
try {
const data = unref(formData);
// 构建数据对象
const item = {
identifier: data.identifier,
name: data.name,
description: data.description,
dataType: IoTDataSpecsDataTypeEnum.STRUCT,
childDataType: data.property.dataType,
dataSpecs:
!!data.property.dataSpecs &&
Object.keys(data.property.dataSpecs).length > 1
? data.property.dataSpecs
: undefined,
dataSpecsList: isEmpty(data.property.dataSpecsList)
? undefined
: data.property.dataSpecsList,
};
// 新增或修改同 identifier 的参数
const existingIndex = dataSpecsList.value.findIndex(
(spec) => spec.identifier === data.identifier,
);
if (existingIndex === -1) {
dataSpecsList.value.push(item);
} else {
dataSpecsList.value[existingIndex] = item;
}
} finally {
dialogVisible.value = false;
}
}
/** 重置表单 */
function resetForm() {
formData.value = {
property: {
dataType: IoTDataSpecsDataTypeEnum.INT,
dataSpecs: {
dataType: IoTDataSpecsDataTypeEnum.INT,
},
dataSpecsList: [],
},
};
structFormRef.value?.resetFields();
}
/** 组件初始化 */
onMounted(async () => {
await nextTick();
// 预防 dataSpecsList 空指针
isEmpty(dataSpecsList.value) && (dataSpecsList.value = []);
});
</script>
<template>
<!-- struct 数据展示 -->
<Form.Item label="属性对象">
<div
v-for="(item, index) in dataSpecsList"
:key="index"
class="px-10px mb-10px flex w-full justify-between bg-gray-100"
>
<span>参数{{ item.name }}</span>
<div class="btn">
<Button type="link" @click="openStructForm(item)"> 编辑 </Button>
<Divider type="vertical" />
<Button type="link" danger @click="deleteStructItem(index)">
删除
</Button>
</div>
</div>
<Button type="link" @click="openStructForm(null)"> +新增参数 </Button>
</Form.Item>
<!-- struct 表单 -->
<Modal
v-model:open="dialogVisible"
:title="dialogTitle"
:confirm-loading="formLoading"
@ok="submitForm"
>
<Form
ref="structFormRef"
:model="formData"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<Form.Item label="参数名称" name="name">
<Input v-model:value="formData.name" placeholder="请输入功能名称" />
</Form.Item>
<Form.Item label="标识符" name="identifier">
<Input v-model:value="formData.identifier" placeholder="请输入标识符" />
</Form.Item>
<!-- 属性配置 -->
<ThingModelProperty v-model="formData.property" is-struct-data-specs />
</Form>
</Modal>
</template>

View File

@@ -9,17 +9,15 @@ import { isEmpty } from '@vben/utils';
import { useVModel } from '@vueuse/core';
import { Form, Radio } from 'ant-design-vue';
import { ThingModelFormRules } from '#/api/iot/thingmodel';
import {
IoTThingModelEventTypeEnum,
IoTThingModelParamDirectionEnum,
} from '#/views/iot/utils/constants';
import ThingModelInputOutputParam from './thing-model-input-output-param.vue';
import ThingModelInputOutputParam from './input-output-param.vue';
/** IoT 物模型事件 */
defineOptions({ name: 'ThingModelEvent' });
const props = defineProps<{ isStructDataSpecs?: boolean; modelValue: any }>();
const props = defineProps<{ modelValue: any }>();
const emits = defineEmits(['update:modelValue']);
const thingModelEvent = useVModel(props, 'modelValue', emits) as Ref<any>;
@@ -36,7 +34,7 @@ watch(
<template>
<Form.Item
:name="['event', 'type']"
:rules="[{ required: true, message: '请选择事件类型', trigger: 'change' }]"
:rules="ThingModelFormRules.eventType"
label="事件类型"
>
<Radio.Group v-model:value="thingModelEvent.type">

View File

@@ -0,0 +1,246 @@
<script lang="ts" setup>
import type { Ref } from 'vue';
import type { IotProductApi } from '#/api/iot/product/product';
import type { ThingModelData } from '#/api/iot/thingmodel';
import { computed, inject, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { $t } from '@vben/locales';
import { cloneDeep, isEmpty } from '@vben/utils';
import { Form, Input, message, Radio } from 'ant-design-vue';
import {
createThingModel,
getThingModel,
ThingModelFormRules,
updateThingModel,
} from '#/api/iot/thingmodel';
import {
IOT_PROVIDE_KEY,
IoTDataSpecsDataTypeEnum,
IoTThingModelTypeEnum,
} from '#/views/iot/utils/constants';
import ThingModelEvent from './event.vue';
import ThingModelProperty from './property.vue';
import ThingModelService from './service.vue';
const emit = defineEmits(['success']);
const product = inject<Ref<IotProductApi.Product>>(IOT_PROVIDE_KEY.PRODUCT);
const formRef = ref();
const formData = ref<ThingModelData>(buildEmptyFormData());
const getTitle = computed(() =>
formData.value.id
? $t('ui.actionTitle.edit', ['物模型'])
: $t('ui.actionTitle.create', ['物模型']),
);
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
try {
await formRef.value?.validate();
} catch {
return;
}
modalApi.lock();
try {
const data = cloneDeep(formData.value);
data.productId = product!.value.id;
data.productKey = product!.value.productKey;
fillExtraAttributes(data);
await (data.id ? updateThingModel(data) : createThingModel(data));
await modalApi.close();
emit('success');
message.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
return;
}
// 每次打开都先重置到空白,避免上一次的状态残留
formData.value = buildEmptyFormData();
formRef.value?.clearValidate?.();
const data = modalApi.getData<{ id?: number }>();
if (!data?.id) {
return;
}
modalApi.lock();
try {
const result = await getThingModel(data.id);
formData.value = normalizeFormData(result);
} finally {
modalApi.unlock();
}
},
});
/** 构造空白表单数据 */
function buildEmptyFormData(): ThingModelData {
return {
type: IoTThingModelTypeEnum.PROPERTY,
dataType: IoTDataSpecsDataTypeEnum.INT,
property: {
dataType: IoTDataSpecsDataTypeEnum.INT,
dataSpecs: {
dataType: IoTDataSpecsDataTypeEnum.INT,
},
},
service: {
inputParams: [],
outputParams: [],
},
event: {
outputParams: [],
},
};
}
/** 回显数据时,规整各分支字段确保子表单可绑定 */
function normalizeFormData(result: ThingModelData): ThingModelData {
const next: any = { ...result, type: Number(result.type) };
if (isEmpty(next.property)) {
next.dataType = IoTDataSpecsDataTypeEnum.INT;
next.property = {
dataType: IoTDataSpecsDataTypeEnum.INT,
dataSpecs: { dataType: IoTDataSpecsDataTypeEnum.INT },
};
} else {
next.property.dataSpecs ??= {};
next.property.dataSpecsList ??= [];
next.property.dataType ??= IoTDataSpecsDataTypeEnum.INT;
}
if (isEmpty(next.service)) {
next.service = { inputParams: [], outputParams: [] };
} else {
next.service.inputParams ??= [];
next.service.outputParams ??= [];
}
if (isEmpty(next.event)) {
next.event = { outputParams: [] };
} else {
next.event.outputParams ??= [];
}
return next;
}
/** 按功能类型将子表单数据回写到顶层,并清理无关分支 */
function fillExtraAttributes(data: any) {
if (data.type === IoTThingModelTypeEnum.PROPERTY) {
removeDataSpecs(data.property);
data.dataType = data.property.dataType;
data.property.identifier = data.identifier;
data.property.name = data.name;
delete data.service;
delete data.event;
} else if (data.type === IoTThingModelTypeEnum.SERVICE) {
removeDataSpecs(data.service);
data.dataType = data.service.dataType;
data.service.identifier = data.identifier;
data.service.name = data.name;
if (isEmpty(data.service.inputParams)) {
delete data.service.inputParams;
}
if (isEmpty(data.service.outputParams)) {
delete data.service.outputParams;
}
delete data.property;
delete data.event;
} else if (data.type === IoTThingModelTypeEnum.EVENT) {
removeDataSpecs(data.event);
data.dataType = data.event.dataType;
data.event.identifier = data.identifier;
data.event.name = data.name;
if (isEmpty(data.event.outputParams)) {
delete data.event.outputParams;
}
delete data.property;
delete data.service;
}
}
/** 清理空的 dataSpecs / dataSpecsList */
function removeDataSpecs(val: any) {
if (isEmpty(val.dataSpecs)) {
delete val.dataSpecs;
}
if (isEmpty(val.dataSpecsList)) {
delete val.dataSpecsList;
}
}
</script>
<template>
<Modal :title="getTitle" class="w-2/5">
<Form
ref="formRef"
:label-col="{ span: 6 }"
:model="formData"
:wrapper-col="{ span: 18 }"
class="mx-4"
>
<Form.Item
:rules="ThingModelFormRules.type"
label="功能类型"
name="type"
>
<Radio.Group v-model:value="formData.type">
<Radio.Button
v-for="dict in getDictOptions(DICT_TYPE.IOT_THING_MODEL_TYPE)"
:key="String(dict.value)"
:value="Number(dict.value)"
>
{{ dict.label }}
</Radio.Button>
</Radio.Group>
</Form.Item>
<Form.Item
:rules="ThingModelFormRules.name"
label="功能名称"
name="name"
>
<Input v-model:value="formData.name" placeholder="请输入功能名称" />
</Form.Item>
<Form.Item
:rules="ThingModelFormRules.identifier"
label="标识符"
name="identifier"
>
<Input v-model:value="formData.identifier" placeholder="请输入标识符" />
</Form.Item>
<!-- 属性配置 -->
<ThingModelProperty
v-if="formData.type === IoTThingModelTypeEnum.PROPERTY"
v-model="formData.property"
/>
<!-- 服务配置 -->
<ThingModelService
v-if="formData.type === IoTThingModelTypeEnum.SERVICE"
v-model="formData.service"
/>
<!-- 事件配置 -->
<ThingModelEvent
v-if="formData.type === IoTThingModelTypeEnum.EVENT"
v-model="formData.event"
/>
<Form.Item label="描述" name="description">
<Input.TextArea
v-model:value="formData.description"
:maxlength="200"
:rows="3"
placeholder="请输入物模型描述"
/>
</Form.Item>
</Form>
</Modal>
</template>

View File

@@ -0,0 +1,152 @@
<script lang="ts" setup>
import type { Ref } from 'vue';
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { isEmpty } from '@vben/utils';
import { useVModel } from '@vueuse/core';
import { Button, Divider, Form, Input } from 'ant-design-vue';
import { ThingModelFormRules } from '#/api/iot/thingmodel';
import { IoTDataSpecsDataTypeEnum } from '#/views/iot/utils/constants';
import ThingModelProperty from './property.vue';
const props = defineProps<{ direction: string; modelValue: any }>();
const emits = defineEmits(['update:modelValue']);
const thingModelParams = useVModel(props, 'modelValue', emits) as Ref<any[]>;
const paramFormRef = ref();
const formData = ref<any>(buildEmptyFormData());
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
try {
await paramFormRef.value?.validate();
} catch {
return;
}
if (!thingModelParams.value) {
thingModelParams.value = [];
}
const data = formData.value;
const item = {
identifier: data.identifier,
name: data.name,
description: data.description,
dataType: data.property.dataType,
paraOrder: 0,
direction: props.direction,
dataSpecs:
!isEmpty(data.property.dataSpecs) &&
Object.keys(data.property.dataSpecs).length > 1
? data.property.dataSpecs
: undefined,
dataSpecsList: isEmpty(data.property.dataSpecsList)
? undefined
: data.property.dataSpecsList,
};
// 按 identifier 去重,存在则更新,否则追加
const existingIndex = thingModelParams.value.findIndex(
(spec) => spec.identifier === data.identifier,
);
if (existingIndex === -1) {
thingModelParams.value.push(item);
} else {
thingModelParams.value[existingIndex] = item;
}
await modalApi.close();
},
onOpenChange(isOpen: boolean) {
if (!isOpen) {
return;
}
formData.value = buildEmptyFormData();
paramFormRef.value?.clearValidate?.();
const data = modalApi.getData<any>();
if (isEmpty(data)) {
return;
}
formData.value = {
identifier: data.identifier ?? '',
name: data.name ?? '',
description: data.description ?? '',
property: {
dataType: data.dataType ?? IoTDataSpecsDataTypeEnum.INT,
dataSpecs: data.dataSpecs ?? {},
dataSpecsList: data.dataSpecsList ?? [],
},
};
},
});
/** 构造空白参数表单 */
function buildEmptyFormData() {
return {
identifier: '',
name: '',
description: '',
property: {
dataType: IoTDataSpecsDataTypeEnum.INT,
dataSpecs: { dataType: IoTDataSpecsDataTypeEnum.INT },
dataSpecsList: [],
},
};
}
/** 打开参数表单(新增或编辑) */
function openParamForm(val: any) {
modalApi.setData(val).open();
}
/** 删除参数项 */
function deleteParamItem(index: number) {
thingModelParams.value.splice(index, 1);
}
</script>
<template>
<div
v-for="(item, index) in thingModelParams"
:key="index"
class="mb-2.5 flex w-full justify-between bg-gray-100 px-2.5 dark:bg-gray-800"
>
<span>参数名称{{ item.name }}</span>
<div>
<Button type="link" @click="openParamForm(item)">编辑</Button>
<Divider type="vertical" />
<Button danger type="link" @click="deleteParamItem(index)">删除</Button>
</div>
</div>
<Button type="link" @click="openParamForm(null)">+ 新增参数</Button>
<!-- 参数表单 -->
<Modal class="w-2/5" title="参数配置">
<Form
ref="paramFormRef"
:label-col="{ span: 6 }"
:model="formData"
:wrapper-col="{ span: 18 }"
class="mx-4"
>
<Form.Item
:rules="ThingModelFormRules.name"
label="参数名称"
name="name"
>
<Input v-model:value="formData.name" placeholder="请输入参数名称" />
</Form.Item>
<Form.Item
:rules="ThingModelFormRules.identifier"
label="标识符"
name="identifier"
>
<Input v-model:value="formData.identifier" placeholder="请输入标识符" />
</Form.Item>
<!-- 属性配置 -->
<ThingModelProperty v-model="formData.property" is-params />
</Form>
</Modal>
</template>

View File

@@ -11,6 +11,7 @@ import { isEmpty } from '@vben/utils';
import { useVModel } from '@vueuse/core';
import { Form, Input, Radio, Select } from 'ant-design-vue';
import { ThingModelFormRules, validateBoolName } from '#/api/iot/thingmodel';
import {
getDataTypeOptions,
IoTDataSpecsDataTypeEnum,
@@ -22,10 +23,22 @@ import {
ThingModelEnumDataSpecs,
ThingModelNumberDataSpecs,
ThingModelStructDataSpecs,
} from './dataSpecs';
} from './data-specs';
/** IoT 物模型属性 */
defineOptions({ name: 'ThingModelProperty' });
/** 嵌套在结构体里时,禁止再选数组 / 结构体(最多支持两层嵌套) */
const NESTED_EXCLUDED_TYPES = new Set<string>([
IoTDataSpecsDataTypeEnum.ARRAY,
IoTDataSpecsDataTypeEnum.STRUCT,
]);
const STRUCT_CHILD_OPTIONS = getDataTypeOptions().filter(
(item) => !NESTED_EXCLUDED_TYPES.has(item.value),
);
/** 数值型数据类型集合 */
const NUMERIC_TYPES = new Set<string>([
IoTDataSpecsDataTypeEnum.INT,
IoTDataSpecsDataTypeEnum.DOUBLE,
IoTDataSpecsDataTypeEnum.FLOAT,
]);
const props = defineProps<{
isParams?: boolean;
@@ -38,65 +51,54 @@ const property = useVModel(
'modelValue',
emits,
) as Ref<ThingModelProperty>;
const getDataTypeOptions2 = computed(() => {
if (!props.isStructDataSpecs) {
return getDataTypeOptions();
}
const excludedTypes = new Set([
IoTDataSpecsDataTypeEnum.ARRAY,
IoTDataSpecsDataTypeEnum.STRUCT,
]);
return getDataTypeOptions().filter(
(item: any) => !excludedTypes.has(item.value),
);
}); //
/** 属性值的数据类型切换时初始化相关数据 */
const dataTypeOptions = computed(() =>
props.isStructDataSpecs ? STRUCT_CHILD_OPTIONS : getDataTypeOptions(),
);
/** 数据类型切换时,重置 dataSpecs / dataSpecsList 并按新类型初始化 */
function handleChange(dataType: any) {
property.value.dataSpecs = {};
property.value.dataSpecsList = [];
// dataSpecs.dataType
![
// / / / dataType dataSpecs / / dataSpecsList
const listLike = [
IoTDataSpecsDataTypeEnum.BOOL,
IoTDataSpecsDataTypeEnum.ENUM,
IoTDataSpecsDataTypeEnum.STRUCT,
].includes(dataType) && (property.value.dataSpecs.dataType = dataType);
switch (dataType) {
case IoTDataSpecsDataTypeEnum.BOOL: {
for (let i = 0; i < 2; i++) {
property.value.dataSpecsList.push({
dataType: IoTDataSpecsDataTypeEnum.BOOL,
name: '', //
value: i, //
});
}
break;
}
case IoTDataSpecsDataTypeEnum.ENUM: {
property.value.dataSpecsList.push({
dataType: IoTDataSpecsDataTypeEnum.ENUM,
name: '', //
value: undefined, //
});
break;
}
];
if (!listLike.includes(dataType)) {
property.value.dataSpecs.dataType = dataType;
}
if (dataType === IoTDataSpecsDataTypeEnum.BOOL) {
for (let i = 0; i < 2; i++) {
property.value.dataSpecsList.push({
dataType: IoTDataSpecsDataTypeEnum.BOOL,
name: '', //
value: i, //
});
}
} else if (dataType === IoTDataSpecsDataTypeEnum.ENUM) {
property.value.dataSpecsList.push({
dataType: IoTDataSpecsDataTypeEnum.ENUM,
name: '', //
value: undefined, //
});
}
// useVModel emit
}
/** 默认选中读写 */
watch(
() => property.value.accessMode,
(val: string | undefined) => {
if (props.isStructDataSpecs || props.isParams) {
return;
}
if (isEmpty(val)) {
property.value.accessMode = IoTThingModelAccessModeEnum.READ_WRITE.value;
}
},
{ immediate: true },
);
/** 顶层属性表单首次进入时,默认选中读写 */
if (!props.isStructDataSpecs && !props.isParams) {
watch(
() => property.value.accessMode,
(val: string | undefined) => {
if (isEmpty(val)) {
property.value.accessMode =
IoTThingModelAccessModeEnum.READ_WRITE.value;
}
},
{ immediate: true },
);
}
</script>
<template>
@@ -108,7 +110,7 @@ watch(
>
<!-- ARRAY STRUCT 类型数据相互嵌套时最多支持递归嵌套 2 父和子 -->
<Select.Option
v-for="option in getDataTypeOptions2"
v-for="option in dataTypeOptions"
:key="option.value"
:value="option.value"
>
@@ -118,13 +120,7 @@ watch(
</Form.Item>
<!-- 数值型配置 -->
<ThingModelNumberDataSpecs
v-if="
[
IoTDataSpecsDataTypeEnum.INT,
IoTDataSpecsDataTypeEnum.DOUBLE,
IoTDataSpecsDataTypeEnum.FLOAT,
].includes(property.dataType || '')
"
v-if="NUMERIC_TYPES.has(property.dataType || '')"
v-model="property.dataSpecs"
/>
<!-- 枚举型配置 -->
@@ -137,17 +133,27 @@ watch(
v-if="property.dataType === IoTDataSpecsDataTypeEnum.BOOL"
label="布尔值"
>
<template v-for="item in property.dataSpecsList" :key="item.value">
<div class="w-1/1 mb-5px flex items-center justify-start">
<template
v-for="(item, index) in property.dataSpecsList"
:key="item.value"
>
<div class="mb-[5px] flex w-full items-center justify-start">
<span>{{ item.value }}</span>
<span class="mx-2">-</span>
<div class="flex-1">
<Form.Item
:name="['property', 'dataSpecsList', index, 'name']"
:rules="[
{ required: true, message: '布尔描述不能为空', trigger: 'blur' },
{ validator: validateBoolName, trigger: 'blur' },
]"
class="mb-0 flex-1"
>
<Input
v-model:value="item.name"
:placeholder="`如:${item.value === 0 ? '关' : '开'}`"
class="w-255px!"
class="!w-[255px]"
/>
</div>
</Form.Item>
</div>
</template>
</Form.Item>
@@ -155,11 +161,12 @@ watch(
<Form.Item
v-if="property.dataType === IoTDataSpecsDataTypeEnum.TEXT"
:name="['property', 'dataSpecs', 'length']"
:rules="ThingModelFormRules.length"
label="数据长度"
>
<Input
v-model:value="property.dataSpecs.length"
class="w-255px!"
class="!w-[255px]"
placeholder="请输入文本字节长度"
>
<template #addonAfter>字节</template>
@@ -172,7 +179,7 @@ watch(
name="date"
>
<Input
class="w-255px!"
class="!w-[255px]"
disabled
placeholder="String 类型的 UTC 时间戳(毫秒)"
/>
@@ -190,6 +197,7 @@ watch(
<Form.Item
v-if="!isStructDataSpecs && !isParams"
:name="['property', 'accessMode']"
:rules="ThingModelFormRules.accessMode"
label="读写类型"
>
<Radio.Group v-model:value="property.accessMode">

View File

@@ -9,17 +9,15 @@ import { isEmpty } from '@vben/utils';
import { useVModel } from '@vueuse/core';
import { Form, Radio } from 'ant-design-vue';
import { ThingModelFormRules } from '#/api/iot/thingmodel';
import {
IoTThingModelParamDirectionEnum,
IoTThingModelServiceCallTypeEnum,
} from '#/views/iot/utils/constants';
import ThingModelInputOutputParam from './thing-model-input-output-param.vue';
import ThingModelInputOutputParam from './input-output-param.vue';
/** IoT 物模型服务 */
defineOptions({ name: 'ThingModelService' });
const props = defineProps<{ isStructDataSpecs?: boolean; modelValue: any }>();
const props = defineProps<{ modelValue: any }>();
const emits = defineEmits(['update:modelValue']);
const service = useVModel(props, 'modelValue', emits) as Ref<any>;
@@ -36,7 +34,7 @@ watch(
<template>
<Form.Item
:name="['service', 'callType']"
:rules="[{ required: true, message: '请选择调用方式', trigger: 'change' }]"
:rules="ThingModelFormRules.callType"
label="调用方式"
>
<Radio.Group v-model:value="service.callType">

View File

@@ -1,298 +0,0 @@
<!-- 产品的物模型表单 -->
<script lang="ts" setup>
import type { Ref } from 'vue';
// TODO @haohao使用 form.vue
import type { IotProductApi } from '#/api/iot/product/product';
import type { ThingModelData } from '#/api/iot/thingmodel';
import { inject, ref } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { $t } from '@vben/locales';
import { cloneDeep } from '@vben/utils';
import { Form, Input, message, Modal, Radio } from 'ant-design-vue';
import {
createThingModel,
getThingModel,
updateThingModel,
} from '#/api/iot/thingmodel';
import {
IOT_PROVIDE_KEY,
IoTDataSpecsDataTypeEnum,
IoTThingModelTypeEnum,
} from '#/views/iot/utils/constants';
import ThingModelEvent from './thing-model-event.vue';
import ThingModelProperty from './thing-model-property.vue';
import ThingModelService from './thing-model-service.vue';
/** IoT 物模型数据表单 */
defineOptions({ name: 'IoTThingModelForm' });
/** 提交表单 */
const emit = defineEmits(['success']);
const product = inject<Ref<IotProductApi.Product>>(IOT_PROVIDE_KEY.PRODUCT); // 注入产品信息
const dialogVisible = ref(false); // 弹窗的是否展示
const dialogTitle = ref(''); // 弹窗的标题
const formLoading = ref(false); // 表单的加载中1修改时的数据加载2提交的按钮禁用
const formType = ref(''); // 表单的类型create - 新增update - 修改
const formData = ref<any>({
type: IoTThingModelTypeEnum.PROPERTY,
dataType: IoTDataSpecsDataTypeEnum.INT,
property: {
dataType: IoTDataSpecsDataTypeEnum.INT,
dataSpecs: {
dataType: IoTDataSpecsDataTypeEnum.INT,
},
},
service: {
inputParams: [],
outputParams: [],
},
event: {
outputParams: [],
},
});
const formRef = ref(); // 表单 Ref
/** 打开弹窗 */
// TODO @haohaoModal 的写法。
async function open(type: string, id?: number) {
dialogVisible.value = true;
// 设置标题create -> 新增update -> 编辑
dialogTitle.value =
type === 'create' ? $t('page.action.add') : $t('page.action.edit');
formType.value = type;
resetForm();
if (id) {
formLoading.value = true;
try {
const result = await getThingModel(id);
// 转换类型为数字
formData.value = {
...result,
type: Number(result.type),
};
// 情况一:属性初始化
if (
!formData.value.property ||
Object.keys(formData.value.property).length === 0
) {
formData.value.dataType = IoTDataSpecsDataTypeEnum.INT;
formData.value.property = {
dataType: IoTDataSpecsDataTypeEnum.INT,
dataSpecs: {
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 (
!formData.value.service ||
Object.keys(formData.value.service).length === 0
) {
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 (
!formData.value.event ||
Object.keys(formData.value.event).length === 0
) {
formData.value.event = {
outputParams: [],
};
} else {
// 确保参数数组存在
if (!formData.value.event.outputParams) {
formData.value.event.outputParams = [];
}
}
} finally {
formLoading.value = false;
}
}
}
defineExpose({ open, close: () => (dialogVisible.value = false) });
async function submitForm() {
await formRef.value.validate();
formLoading.value = true;
try {
const data = cloneDeep(formData.value) as ThingModelData;
// 信息补全
data.productId = product!.value.id;
data.productKey = product!.value.productKey;
fillExtraAttributes(data);
await (formType.value === 'create'
? createThingModel(data)
: updateThingModel(data));
message.success($t('ui.actionMessage.operationSuccess'));
// 关闭弹窗
dialogVisible.value = false;
emit('success');
} finally {
formLoading.value = false;
}
}
/** 填写额外的属性(处理不同类型的情况) */
function fillExtraAttributes(data: any) {
// 属性
if (data.type === IoTThingModelTypeEnum.PROPERTY) {
removeDataSpecs(data.property);
data.dataType = data.property.dataType;
data.property.identifier = data.identifier;
data.property.name = data.name;
delete data.service;
delete data.event;
}
// 服务
if (data.type === IoTThingModelTypeEnum.SERVICE) {
removeDataSpecs(data.service);
data.dataType = data.service.dataType;
data.service.identifier = data.identifier;
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.event;
}
// 事件
if (data.type === IoTThingModelTypeEnum.EVENT) {
removeDataSpecs(data.event);
data.dataType = data.event.dataType;
data.event.identifier = data.identifier;
data.event.name = data.name;
// 保留输出参数,但如果为空数组则删除
if (!data.event.outputParams || data.event.outputParams.length === 0) {
delete data.event.outputParams;
}
delete data.property;
delete data.service;
}
}
/** 处理 dataSpecs 为空的情况 */
function removeDataSpecs(val: any) {
if (!val.dataSpecs || Object.keys(val.dataSpecs).length === 0) {
delete val.dataSpecs;
}
if (!val.dataSpecsList || val.dataSpecsList.length === 0) {
delete val.dataSpecsList;
}
}
/** 重置表单 */
function resetForm() {
formData.value = {
type: IoTThingModelTypeEnum.PROPERTY,
dataType: IoTDataSpecsDataTypeEnum.INT,
property: {
dataType: IoTDataSpecsDataTypeEnum.INT,
dataSpecs: {
dataType: IoTDataSpecsDataTypeEnum.INT,
},
},
service: {
inputParams: [],
outputParams: [],
},
event: {
outputParams: [],
},
};
formRef.value?.resetFields();
}
</script>
<template>
<Modal
v-model:open="dialogVisible"
:title="dialogTitle"
:confirm-loading="formLoading"
@ok="submitForm"
>
<!-- TODO @haohao这个可以改造成 data.ts schema 形式么可能是有一定成本后续迁移 ele 版本会容易很多 -->
<Form
ref="formRef"
:model="formData"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<Form.Item label="功能类型" name="type">
<Radio.Group v-model:value="formData.type">
<Radio.Button
v-for="dict in getDictOptions(DICT_TYPE.IOT_THING_MODEL_TYPE)"
:key="String(dict.value)"
:value="Number(dict.value)"
>
{{ dict.label }}
</Radio.Button>
</Radio.Group>
</Form.Item>
<Form.Item label="功能名称" name="name">
<Input v-model:value="formData.name" placeholder="请输入功能名称" />
</Form.Item>
<Form.Item label="标识符" name="identifier">
<Input v-model:value="formData.identifier" placeholder="请输入标识符" />
</Form.Item>
<!-- 属性配置 -->
<ThingModelProperty
v-if="formData.type === IoTThingModelTypeEnum.PROPERTY"
v-model="formData.property"
/>
<!-- 服务配置 -->
<ThingModelService
v-if="formData.type === IoTThingModelTypeEnum.SERVICE"
v-model="formData.service"
/>
<!-- 事件配置 -->
<ThingModelEvent
v-if="formData.type === IoTThingModelTypeEnum.EVENT"
v-model="formData.event"
/>
<Form.Item label="描述" name="desc">
<Input.TextArea
v-model:value="formData.desc"
:maxlength="200"
:rows="3"
placeholder="请输入属性描述"
/>
</Form.Item>
</Form>
</Modal>
</template>

View File

@@ -1,163 +0,0 @@
<!-- 产品的物模型表单eventservice 项里的参数 -->
<script lang="ts" setup>
import type { Ref } from 'vue';
import { ref, unref } from 'vue';
import { isEmpty } from '@vben/utils';
import { useVModel } from '@vueuse/core';
import { Button, Divider, Form, Input, Modal } from 'ant-design-vue';
import { IoTDataSpecsDataTypeEnum } from '#/views/iot/utils/constants';
import ThingModelProperty from './thing-model-property.vue';
/** 输入输出参数配置组件 */
defineOptions({ name: 'ThingModelInputOutputParam' });
const props = defineProps<{ direction: string; modelValue: any }>();
const emits = defineEmits(['update:modelValue']);
const thingModelParams = useVModel(props, 'modelValue', emits) as Ref<any[]>;
const dialogVisible = ref(false); // 弹窗的是否展示
const formLoading = ref(false); // 表单的加载中1修改时的数据加载2提交的按钮禁用
const paramFormRef = ref(); // 表单 ref
const formData = ref<any>({
dataType: IoTDataSpecsDataTypeEnum.INT,
property: {
dataType: IoTDataSpecsDataTypeEnum.INT,
dataSpecs: {
dataType: IoTDataSpecsDataTypeEnum.INT,
},
dataSpecsList: [],
},
});
/** 打开 param 表单 */
function openParamForm(val: any) {
dialogVisible.value = true;
resetForm();
if (isEmpty(val)) {
return;
}
// 编辑时回显数据
const valData = val as any;
formData.value = {
identifier: valData?.identifier || '',
name: valData?.name || '',
description: valData?.description || '',
property: {
dataType: valData?.dataType || IoTDataSpecsDataTypeEnum.INT,
dataSpecs: valData?.dataSpecs ?? {},
dataSpecsList: valData?.dataSpecsList ?? [],
},
};
// 确保 property.dataType 有值
if (!formData.value.property.dataType) {
formData.value.property.dataType = IoTDataSpecsDataTypeEnum.INT;
}
}
/** 删除 param 项 */
function deleteParamItem(index: number) {
thingModelParams.value.splice(index, 1);
}
/** 添加参数 */
async function submitForm() {
// 初始化参数列表
if (isEmpty(thingModelParams.value)) {
thingModelParams.value = [];
}
// 校验参数
await paramFormRef.value.validate();
try {
// 构建数据对象
const data = unref(formData);
const item = {
identifier: data.identifier,
name: data.name,
description: data.description,
dataType: data.property.dataType,
paraOrder: 0, // TODO @puhui999: 先写死默认看看后续
direction: props.direction,
dataSpecs:
!!data.property.dataSpecs &&
Object.keys(data.property.dataSpecs).length > 1
? data.property.dataSpecs
: undefined,
dataSpecsList: isEmpty(data.property.dataSpecsList)
? undefined
: data.property.dataSpecsList,
};
// 新增或修改同 identifier 的参数
const existingIndex = thingModelParams.value.findIndex(
(spec) => spec.identifier === data.identifier,
);
if (existingIndex === -1) {
thingModelParams.value.push(item);
} else {
thingModelParams.value[existingIndex] = item;
}
} finally {
dialogVisible.value = false;
}
}
/** 重置表单 */
function resetForm() {
formData.value = {
dataType: IoTDataSpecsDataTypeEnum.INT,
property: {
dataType: IoTDataSpecsDataTypeEnum.INT,
dataSpecs: {
dataType: IoTDataSpecsDataTypeEnum.INT,
},
dataSpecsList: [],
},
};
paramFormRef.value?.resetFields();
}
</script>
<template>
<div
v-for="(item, index) in thingModelParams"
:key="index"
class="w-1/1 px-10px mb-10px flex justify-between bg-gray-100"
>
<span>参数名称{{ item.name }}</span>
<div class="btn">
<Button type="link" @click="openParamForm(item)">编辑</Button>
<Divider type="vertical" />
<Button type="link" danger @click="deleteParamItem(index)">删除</Button>
</div>
</div>
<Button type="link" @click="openParamForm(null)">+新增参数</Button>
<!-- param 表单 -->
<Modal
v-model:open="dialogVisible"
title="新增参数"
:confirm-loading="formLoading"
@ok="submitForm"
>
<Form
ref="paramFormRef"
:model="formData"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<Form.Item label="参数名称" name="name">
<Input v-model:value="formData.name" placeholder="请输入功能名称" />
</Form.Item>
<Form.Item label="标识符" name="identifier">
<Input v-model:value="formData.identifier" placeholder="请输入标识符" />
</Form.Item>
<!-- 属性配置 -->
<ThingModelProperty v-model="formData.property" is-params />
</Form>
</Modal>
</template>

View File

@@ -1,117 +0,0 @@
<script setup lang="ts">
import type { Ref } from 'vue';
import type { IotProductApi } from '#/api/iot/product/product';
import { computed, inject, ref, watch } from 'vue';
import { Modal, Radio, Textarea } from 'ant-design-vue';
import { getThingModelTSL } from '#/api/iot/thingmodel';
import { IOT_PROVIDE_KEY } from '#/views/iot/utils/constants';
defineOptions({ name: 'ThingModelTsl' });
const dialogVisible = ref(false); // 弹窗的是否展示
const dialogTitle = ref('物模型 TSL'); // 弹窗的标题
const product = inject<Ref<IotProductApi.Product>>(IOT_PROVIDE_KEY.PRODUCT); // 注入产品信息
const viewMode = ref('view'); // 查看模式view-代码视图editor-编辑器视图
/** 打开弹窗 */
async function open() {
dialogVisible.value = true;
await getTsl();
}
defineExpose({ open });
/** 获取 TSL */
const thingModelTSL = ref<any>({});
const tslString = ref(''); // 用于编辑器的字符串格式
async function getTsl() {
try {
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 用于只读展示 */
const formattedTSL = computed(() => {
try {
if (typeof thingModelTSL.value === 'string') {
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>
<template>
<Modal
v-model:open="dialogVisible"
:title="dialogTitle"
:footer="null"
width="800px"
>
<div class="mb-4">
<Radio.Group v-model:value="viewMode" size="small">
<Radio.Button value="view">代码视图</Radio.Button>
<Radio.Button value="editor">编辑器视图</Radio.Button>
</Radio.Group>
</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>
</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;
overflow-wrap: break-word;
white-space: pre-wrap;
}
.json-editor {
font-family: Monaco, Menlo, 'Ubuntu Mono', Consolas, monospace;
font-size: 13px;
}
</style>

View File

@@ -0,0 +1,79 @@
<script setup lang="ts">
import type { Ref } from 'vue';
import type { IotProductApi } from '#/api/iot/product/product';
import { computed, inject, ref, watch } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Radio, Textarea } from 'ant-design-vue';
import { getThingModelTSL } from '#/api/iot/thingmodel';
import { IOT_PROVIDE_KEY } from '#/views/iot/utils/constants';
const product = inject<Ref<IotProductApi.Product>>(IOT_PROVIDE_KEY.PRODUCT);
const viewMode = ref<'editor' | 'view'>('view');
const thingModelTSL = ref<any>({});
const tslString = ref('');
const [Modal, modalApi] = useVbenModal({
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
return;
}
modalApi.lock();
try {
thingModelTSL.value = await getThingModelTSL(product?.value?.id || 0);
tslString.value = JSON.stringify(thingModelTSL.value, null, 2);
} finally {
modalApi.unlock();
}
},
});
/** 只读视图下,格式化后的 TSL 字符串 */
const formattedTSL = computed(() =>
JSON.stringify(thingModelTSL.value, null, 2),
);
/** 编辑器内容变化时,同步到数据对象 */
watch(tslString, (newValue) => {
try {
thingModelTSL.value = JSON.parse(newValue);
} catch {
// JSON 解析失败时保持原值
}
});
</script>
<template>
<Modal :footer="false" class="w-3/5" title="物模型 TSL">
<div class="mx-4">
<div class="mb-4">
<Radio.Group v-model:value="viewMode" size="small">
<Radio.Button value="view">代码视图</Radio.Button>
<Radio.Button value="editor">编辑器视图</Radio.Button>
</Radio.Group>
</div>
<!-- 代码视图只读展示 -->
<div
v-if="viewMode === 'view'"
class="max-h-[600px] overflow-y-auto rounded border border-gray-200 bg-gray-50 p-3 dark:border-gray-700 dark:bg-gray-800"
>
<pre
class="m-0 whitespace-pre-wrap break-words font-mono text-[13px] leading-normal"
><code>{{ formattedTSL }}</code></pre>
</div>
<!-- 编辑器视图可编辑 -->
<Textarea
v-else
v-model:value="tslString"
:rows="20"
class="font-mono text-[13px]"
placeholder="请输入 JSON 格式的物模型 TSL"
/>
</div>
</Modal>
</template>

View File

@@ -1,9 +1,6 @@
// TODO @AI感觉这块放到 biz-iot-enum 里好点。
/** 检查值是否为空 */
const isEmpty = (value: any): boolean => {
return value === null || value === undefined || value === '';
};
import { isEmpty } from '@vben/utils';
/** IoT 依赖注入 KEY */
export const IOT_PROVIDE_KEY = {
@@ -104,19 +101,19 @@ export const IoTDataSpecsDataTypeEnum = {
ARRAY: 'array',
};
export const getDataTypeOptions = () => {
return [
{ value: IoTDataSpecsDataTypeEnum.INT, label: '整数型' },
{ value: IoTDataSpecsDataTypeEnum.FLOAT, label: '精度浮点型' },
{ value: IoTDataSpecsDataTypeEnum.DOUBLE, label: '双精度浮点型' },
{ value: IoTDataSpecsDataTypeEnum.ENUM, label: '枚举型' },
{ value: IoTDataSpecsDataTypeEnum.BOOL, label: '布尔型' },
{ value: IoTDataSpecsDataTypeEnum.TEXT, label: '文本型' },
{ value: IoTDataSpecsDataTypeEnum.DATE, label: '时间型' },
{ value: IoTDataSpecsDataTypeEnum.STRUCT, label: '结构体' },
{ value: IoTDataSpecsDataTypeEnum.ARRAY, label: '数组' },
];
};
const DATA_TYPE_OPTIONS = Object.freeze([
{ value: IoTDataSpecsDataTypeEnum.INT, label: '整数型' },
{ value: IoTDataSpecsDataTypeEnum.FLOAT, label: '单精度浮点型' },
{ value: IoTDataSpecsDataTypeEnum.DOUBLE, label: '精度浮点型' },
{ value: IoTDataSpecsDataTypeEnum.ENUM, label: '枚举型' },
{ value: IoTDataSpecsDataTypeEnum.BOOL, label: '布尔型' },
{ value: IoTDataSpecsDataTypeEnum.TEXT, label: '文本型' },
{ value: IoTDataSpecsDataTypeEnum.DATE, label: '时间型' },
{ value: IoTDataSpecsDataTypeEnum.STRUCT, label: '结构体' },
{ value: IoTDataSpecsDataTypeEnum.ARRAY, label: '数组' },
]);
export const getDataTypeOptions = () => DATA_TYPE_OPTIONS;
/** 获得物体模型数据类型配置项名称 */
export const getDataTypeOptionsLabel = (value: string) => {

View File

@@ -41,6 +41,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '品牌排序',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
placeholder: '请输入品牌排序',
},

View File

@@ -65,6 +65,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '分类排序',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
placeholder: '请输入分类排序',
},

View File

@@ -281,6 +281,7 @@ export function useOtherFormSchema(): VbenFormSchema[] {
label: '商品排序',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
},
rules: 'required',
@@ -290,6 +291,7 @@ export function useOtherFormSchema(): VbenFormSchema[] {
label: '赠送积分',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
},
rules: 'required',
@@ -299,6 +301,7 @@ export function useOtherFormSchema(): VbenFormSchema[] {
label: '虚拟销量',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
},
rules: 'required',

View File

@@ -40,6 +40,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '显示顺序',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
placeholder: '请输入显示顺序',
},

View File

@@ -106,6 +106,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '排序',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
placeholder: '请输入排序',
},

View File

@@ -62,6 +62,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '排序',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
placeholder: '请输入排序',
},

View File

@@ -55,6 +55,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '助力人数',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 1,
placeholder: '达到该人数才能砍到低价',
},
@@ -65,6 +66,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '砍价次数',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 1,
placeholder: '最大帮砍次数',
},
@@ -75,6 +77,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '购买限制',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 1,
placeholder: '最大购买次数',
},
@@ -85,6 +88,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '最小砍价金额(元)',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
precision: 2,
step: 0.01,
@@ -96,6 +100,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '最大砍价金额(元)',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
precision: 2,
step: 0.01,

View File

@@ -55,6 +55,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '拼团人数',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '达到该人数即成团',
min: 2,
},
@@ -65,6 +66,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '限制时长',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '限制时长(小时)',
min: 0,
},
@@ -75,6 +77,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '总限购数量',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入总限购数量',
min: 0,
},
@@ -84,6 +87,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '单次限购数量',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入单次限购数量',
min: 0,
},

View File

@@ -122,6 +122,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '优惠券面额',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
precision: 2,
placeholder: '请输入优惠金额,单位:元',
@@ -139,6 +140,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '优惠券折扣',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 1,
max: 9.9,
precision: 1,
@@ -157,6 +159,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '最多优惠',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
precision: 2,
placeholder: '请输入最多优惠',
@@ -174,6 +177,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '满多少元可以使用',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
precision: 2,
placeholder: '无门槛请设为 0',
@@ -196,6 +200,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '发放数量',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: -1,
placeholder: '发放数量,没有之后不能领取或发放,-1 为不限制',
addonAfter: '张',
@@ -212,6 +217,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '每人限领个数',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: -1,
placeholder: '设置为 -1 时,可无限领取',
addonAfter: '张',
@@ -255,6 +261,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '领取日期',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
placeholder: '第 0 为今天生效',
addonBefore: '第',
@@ -271,6 +278,7 @@ export function useFormSchema(): VbenFormSchema[] {
fieldName: 'fixedEndTerm',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
placeholder: '请输入结束天数',
addonBefore: '至',

View File

@@ -109,9 +109,9 @@ export function useFormSchema(): VbenFormSchema[] {
label: '排序',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
placeholder: '请输入排序',
class: '!w-full',
},
defaultValue: 0,
rules: 'required',

View File

@@ -99,9 +99,9 @@ export function useFormSchema(): VbenFormSchema[] {
label: '总限购数量',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入总限购数量',
min: 0,
class: 'w-full',
},
rules: z.number().min(0).default(0),
},
@@ -110,9 +110,9 @@ export function useFormSchema(): VbenFormSchema[] {
label: '单次限购数量',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入单次限购数量',
min: 0,
class: 'w-full',
},
rules: z.number().min(0).default(0),
},
@@ -121,9 +121,9 @@ export function useFormSchema(): VbenFormSchema[] {
label: '排序',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入排序',
min: 0,
class: 'w-full',
},
rules: z.number().min(0).default(0),
},

View File

@@ -63,10 +63,10 @@ export const schema: VbenFormSchema[] = [
label: '满额包邮',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
precision: 2,
placeholder: '请输入满额包邮金额',
class: 'w-full',
},
rules: 'required',
dependencies: {
@@ -145,10 +145,10 @@ export const schema: VbenFormSchema[] = [
label: '一级返佣比例(%',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
max: 100,
placeholder: '请输入一级返佣比例',
class: 'w-full',
},
rules: 'required',
dependencies: {
@@ -162,10 +162,10 @@ export const schema: VbenFormSchema[] = [
label: '二级返佣比例(%',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
max: 100,
placeholder: '请输入二级返佣比例',
class: 'w-full',
},
rules: 'required',
dependencies: {
@@ -179,9 +179,9 @@ export const schema: VbenFormSchema[] = [
label: '佣金冻结天数',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
placeholder: '请输入佣金冻结天数',
class: 'w-full',
},
rules: 'required',
dependencies: {
@@ -195,10 +195,10 @@ export const schema: VbenFormSchema[] = [
label: '提现最低金额(元)',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
precision: 2,
placeholder: '请输入提现最低金额',
class: 'w-full',
},
rules: 'required',
dependencies: {
@@ -212,11 +212,11 @@ export const schema: VbenFormSchema[] = [
label: '提现手续费(%',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
max: 100,
precision: 2,
placeholder: '请输入提现手续费百分比',
class: 'w-full',
},
rules: 'required',
dependencies: {

View File

@@ -47,6 +47,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '显示顺序',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
placeholder: '请输入显示顺序',
},

View File

@@ -140,6 +140,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '显示顺序',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入显示顺序',
min: 0,
},

View File

@@ -306,6 +306,7 @@ export function usePriceFormSchema(): VbenFormSchema[] {
label: '订单调价',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入订单调价',
step: 0.1,
precision: 2,

View File

@@ -21,9 +21,9 @@ export const schema: VbenFormSchema[] = [
label: '积分抵扣',
help: '积分抵用比例(1 积分抵多少金额),单位:元',
componentProps: {
class: '!w-full',
min: 0,
precision: 2,
class: 'w-full',
placeholder: '请输入积分抵扣单价',
},
},
@@ -33,8 +33,8 @@ export const schema: VbenFormSchema[] = [
label: '积分抵扣最大值',
help: '单次下单积分使用上限0 不限制',
componentProps: {
class: '!w-full',
min: 0,
class: 'w-full',
placeholder: '请输入积分抵扣最大值',
},
},
@@ -44,8 +44,8 @@ export const schema: VbenFormSchema[] = [
label: '1 元赠送多少分',
help: '下单支付金额按比例赠送积分(实际支付 1 元赠送多少积分)',
componentProps: {
class: '!w-full',
min: 0,
class: 'w-full',
placeholder: '请输入赠送积分比例',
},
},

View File

@@ -32,6 +32,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '等级',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
precision: 0,
placeholder: '请输入等级',
@@ -43,6 +44,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '升级经验',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
precision: 0,
placeholder: '请输入升级经验',
@@ -54,6 +56,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '享受折扣(%)',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
max: 100,
precision: 0,

View File

@@ -23,6 +23,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '签到天数',
help: '只允许设置 1-7默认签到 7 天为一个周期',
componentProps: {
class: '!w-full',
min: 1,
max: 7,
precision: 0,
@@ -35,6 +36,7 @@ export function useFormSchema(): VbenFormSchema[] {
fieldName: 'point',
label: '获得积分',
componentProps: {
class: '!w-full',
min: 0,
precision: 0,
placeholder: '请输入获得积分',
@@ -46,6 +48,7 @@ export function useFormSchema(): VbenFormSchema[] {
fieldName: 'experience',
label: '奖励经验',
componentProps: {
class: '!w-full',
min: 0,
precision: 0,
placeholder: '请输入奖励经验',

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