Merge remote-tracking branch 'yudao/dev' into dev

This commit is contained in:
jason
2025-11-23 12:00:12 +08:00
226 changed files with 1681 additions and 1683 deletions

View File

@@ -3,7 +3,8 @@ import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace IotDeviceApi {
/** IoT 设备 VO */
// TODO @haohao需要跟后端对齐必要的 ReqVO、RespVO
/** 设备 */
export interface Device {
id?: number; // 设备 ID主键自增
deviceName: string; // 设备名称
@@ -48,7 +49,7 @@ export namespace IotDeviceApi {
dataSpecsList: any[]; // 数据定义列表
}
/** IoT 设备属性 VO */
/** 设备属性 VO */
export interface DeviceProperty {
identifier: string; // 属性标识符
value: string; // 最新值
@@ -62,7 +63,7 @@ export namespace IotDeviceApi {
password: string; // 密码
}
/** IoT 设备发送消息 Request VO */
/** 设备发送消息 Request VO */
export interface DeviceMessageSendReq {
deviceId: number; // 设备编号
method: string; // 请求方法
@@ -77,6 +78,7 @@ export namespace IotDeviceApi {
}
/** IoT 设备状态枚举 */
// TODO @haohaopackages/constants/src/biz-iot-enum.ts 枚举;
export enum DeviceStateEnum {
INACTIVE = 0, // 未激活
OFFLINE = 2, // 离线

View File

@@ -3,7 +3,7 @@ import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace IotDeviceGroupApi {
/** IoT 设备分组 VO */
/** 设备分组 */
export interface DeviceGroup {
id?: number; // 分组 ID
name: string; // 分组名字

View File

@@ -3,19 +3,19 @@ import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace IotProductCategoryApi {
/** IoT 產品分類 VO */
/** 产品分类 */
export interface ProductCategory {
id?: number; // 分 ID
name: string; // 分類名稱
parentId?: number; // 父级分 ID
sort?: number; // 分排序
status?: number; // 分類狀態
description?: string; // 分描述
createTime?: string; // 創建時間
id?: number; // 分 ID
name: string; // 分类名称
parentId?: number; // 父级分 ID
sort?: number; // 分排序
status?: number; // 分类状态
description?: string; // 分描述
createTime?: string; // 创建时间
}
}
/** 查詢產品分類分頁 */
/** 查询产品分类分页 */
export function getProductCategoryPage(params: PageParam) {
return requestClient.get<PageResult<IotProductCategoryApi.ProductCategory>>(
'/iot/product-category/page',
@@ -23,33 +23,33 @@ export function getProductCategoryPage(params: PageParam) {
);
}
/** 查詢產品分類詳情 */
/** 查询产品分类详情 */
export function getProductCategory(id: number) {
return requestClient.get<IotProductCategoryApi.ProductCategory>(
`/iot/product-category/get?id=${id}`,
);
}
/** 新增品分 */
/** 新增品分 */
export function createProductCategory(
data: IotProductCategoryApi.ProductCategory,
) {
return requestClient.post('/iot/product-category/create', data);
}
/** 修改品分 */
/** 修改品分 */
export function updateProductCategory(
data: IotProductCategoryApi.ProductCategory,
) {
return requestClient.put('/iot/product-category/update', data);
}
/** 刪除品分 */
/** 刪除品分 */
export function deleteProductCategory(id: number) {
return requestClient.delete(`/iot/product-category/delete?id=${id}`);
}
/** 獲取產品分類精簡列表 */
/** 获取产品分类精简列表 */
export function getSimpleProductCategoryList() {
return requestClient.get<IotProductCategoryApi.ProductCategory[]>(
'/iot/product-category/simple-list',

View File

@@ -3,7 +3,7 @@ import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace IotProductApi {
/** IoT 产品 VO */
/** 产品 */
export interface Product {
id?: number; // 产品编号
name: string; // 产品名称
@@ -27,6 +27,8 @@ export namespace IotProductApi {
}
}
// TODO @haohaopackages/constants/src/biz-iot-enum.ts 枚举;
/** IOT 产品设备类型枚举类 */
export enum DeviceTypeEnum {
DEVICE = 0, // 直连设备

View File

@@ -3,7 +3,7 @@ import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace RuleSceneApi {
/** IoT 场景联动规则 VO */
/** 场景联动规则 */
export interface SceneRule {
id?: number;
name: string;
@@ -14,7 +14,7 @@ export namespace RuleSceneApi {
createTime?: Date;
}
/** IoT 场景联动规则触发器 */
/** 场景联动规则触发器 */
export interface Trigger {
type?: string;
productId?: number;
@@ -26,13 +26,13 @@ export namespace RuleSceneApi {
conditionGroups?: TriggerConditionGroup[];
}
/** IoT 场景联动规则触发条件组 */
/** 场景联动规则触发条件组 */
export interface TriggerConditionGroup {
conditions?: TriggerCondition[];
operator?: string;
}
/** IoT 场景联动规则触发条件 */
/** 场景联动规则触发条件 */
export interface TriggerCondition {
productId?: number;
deviceId?: number;
@@ -42,7 +42,7 @@ export namespace RuleSceneApi {
type?: string;
}
/** IoT 场景联动规则动作 */
/** 场景联动规则动作 */
export interface Action {
type?: string;
productId?: number;
@@ -53,6 +53,7 @@ export namespace RuleSceneApi {
}
}
// TODO @haohao貌似下面的和 RuleSceneApi 重复了。
/** IoT 场景联动规则 */
export interface IotSceneRule {
id?: number;
@@ -135,6 +136,7 @@ export function deleteSceneRule(id: number) {
}
/** 批量删除场景联动规则 */
// TODO @haohao貌似用上。
export function deleteSceneRuleList(ids: number[]) {
return requestClient.delete('/iot/scene-rule/delete-list', {
params: { ids: ids.join(',') },

View File

@@ -1,7 +1,8 @@
import { requestClient } from '#/api/request';
export namespace IotStatisticsApi {
/** IoT 统计摘要数据 */
// TODO @haohao需要跟后端对齐必要的 ReqVO、RespVO
/** 统计摘要数据 */
export interface StatisticsSummary {
productCategoryCount: number;
productCount: number;
@@ -22,7 +23,7 @@ export namespace IotStatisticsApi {
[key: string]: number;
}
/** IoT 消息统计数据类型 */
/** 消息统计数据类型 */
export interface DeviceMessageSummary {
statType: number;
upstreamCounts: TimeValueItem[];
@@ -60,6 +61,7 @@ export function getDeviceMessageSummaryByDate(
);
}
// TODO @haohao貌似这里没用到是不是后面哪里用或者可以删除哈
/** 获取设备消息统计摘要 */
export function getDeviceMessageSummary(statType: number) {
return requestClient.get<IotStatisticsApi.DeviceMessageSummary>(

View File

@@ -140,19 +140,11 @@ export function getThingModel(id: number) {
/** 根据产品 ID 查询物模型列表 */
export function getThingModelListByProductId(productId: number) {
return requestClient.get<ThingModelApi.ThingModel[]>(
'/iot/thing-model/list-by-product-id',
'/iot/thing-model/list',
{ params: { productId } },
);
}
/** 根据产品标识查询物模型列表 */
export function getThingModelListByProductKey(productKey: string) {
return requestClient.get<ThingModelApi.ThingModel[]>(
'/iot/thing-model/list-by-product-key',
{ params: { productKey } },
);
}
/** 新增物模型 */
export function createThingModel(data: ThingModelData) {
return requestClient.post('/iot/thing-model/create', data);
@@ -168,13 +160,6 @@ export function deleteThingModel(id: number) {
return requestClient.delete(`/iot/thing-model/delete?id=${id}`);
}
/** 批量删除物模型 */
export function deleteThingModelList(ids: number[]) {
return requestClient.delete('/iot/thing-model/delete-list', {
params: { ids: ids.join(',') },
});
}
/** 获取物模型 TSL */
export function getThingModelTSL(productId: number) {
return requestClient.get<ThingModelApi.ThingModel[]>(

View File

@@ -24,6 +24,49 @@ export namespace MpDraftApi {
articles: Article[];
createTime?: Date;
}
/** 图文项(包含预览字段) */
export interface NewsItem {
title: string;
thumbMediaId: string;
author: string;
digest: string;
showCoverPic: number;
content: string;
contentSourceUrl: string;
needOpenComment: number;
onlyFansCanComment: number;
thumbUrl: string;
picUrl?: string; // 用于预览封面
}
/** 图文列表 */
export interface NewsItemList {
newsItem: NewsItem[];
}
/** 草稿文章(用于展示) */
export interface DraftArticle {
mediaId: string;
content: NewsItemList;
updateTime: number;
}
}
/** 创建空的图文项 */
export function createEmptyNewsItem(): MpDraftApi.NewsItem {
return {
title: '',
thumbMediaId: '',
author: '',
digest: '',
showCoverPic: 0,
content: '',
contentSourceUrl: '',
needOpenComment: 0,
onlyFansCanComment: 0,
thumbUrl: '',
};
}
/** 查询草稿列表 */

View File

@@ -1,15 +1,8 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
import { MaterialType } from '@vben/constants';
/** 素材类型枚举 */
// TODO @hwmp 相关的枚举,可以考虑放这里来。
export enum MaterialType {
IMAGE = 1, // 图片
THUMB = 4, // 缩略图
VIDEO = 3, // 视频
VOICE = 2, // 语音
}
import { requestClient } from '#/api/request';
export namespace MpMaterialApi {
/** 素材信息 */

View File

@@ -1,20 +1,6 @@
import { requestClient } from '#/api/request';
import { MenuType } from '@vben/constants';
/** 菜单类型枚举 */
// TODO @hwmp 相关的枚举,可以考虑放这里来。
export enum MenuType {
CLICK = 'click', // 点击推事件
LOCATION_SELECT = 'location_select', // 发送位置
MEDIA_ID = 'media_id', // 下发消息
MINIPROGRAM = 'miniprogram', // 小程序
PIC_PHOTO_OR_ALBUM = 'pic_photo_or_album', // 拍照或者相册发图
PIC_SYSPHOTO = 'pic_sysphoto', // 系统拍照发图
PIC_WEIXIN = 'pic_weixin', // 微信相册发图
SCANCODE_PUSH = 'scancode_push', // 扫码推事件
SCANCODE_WAITMSG = 'scancode_waitmsg', // 扫码带提示
VIEW = 'view', // 跳转 URL
VIEW_LIMITED = 'view_limited', // 跳转图文消息URL
}
import { requestClient } from '#/api/request';
export namespace MpMenuApi {
/** 菜单按钮信息 */

View File

@@ -1,19 +1,8 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
import { MessageType } from '@vben/constants';
/** 消息类型枚举 */
// TODO @hwmp 相关的枚举,可以考虑放这里来。
export enum MessageType {
IMAGE = 'image', // 图片消息
MPNEWS = 'mpnews', // 公众号图文消息
MUSIC = 'music', // 音乐消息
NEWS = 'news', // 图文消息
TEXT = 'text', // 文本消息
VIDEO = 'video', // 视频消息
VOICE = 'voice', // 语音消息
WXCARD = 'wxcard', // 卡券消息
}
import { requestClient } from '#/api/request';
export namespace MpMessageApi {
/** 消息信息 */

View File

@@ -21,15 +21,21 @@ import {
/** 编码表单 Conf */
export function encodeConf(designerRef: any) {
return JSON.stringify(designerRef.value.getOption());
// 关联案例https://gitee.com/yudaocode/yudao-ui-admin-vue3/pulls/834/
return formCreate.toJson(designerRef.value.getOption());
}
/** 解码表单 Conf */
export function decodeConf(conf: string) {
return formCreate.parseJson(conf);
}
/** 编码表单 Fields */
export function encodeFields(designerRef: any) {
const rule = JSON.parse(designerRef.value.getJson());
const rule = designerRef.value.getRule();
const fields: string[] = [];
rule.forEach((item: unknown) => {
fields.push(JSON.stringify(item));
rule.forEach((item: any) => {
fields.push(formCreate.toJson(item));
});
return fields;
}
@@ -49,7 +55,7 @@ export function setConfAndFields(
conf: string,
fields: string | string[],
) {
designerRef.value.setOption(formCreate.parseJson(conf));
designerRef.value.setOption(decodeConf(conf));
// 处理 fields 参数类型,确保传入 decodeFields 的是 string[] 类型
const fieldsArray = Array.isArray(fields) ? fields : [fields];
designerRef.value.setRule(decodeFields(fieldsArray));
@@ -65,7 +71,7 @@ export function setConfAndFields2(
if (isRef(detailPreview)) {
detailPreview = detailPreview.value;
}
detailPreview.option = formCreate.parseJson(conf);
detailPreview.option = decodeConf(conf);
detailPreview.rule = decodeFields(fields);
if (value) {
detailPreview.value = value;

View File

@@ -10,7 +10,7 @@ import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteAlertConfig, getAlertConfigPage } from '#/api/iot/alert/config';
import { $t } from '#/locales';
import AlertConfigForm from '../modules/AlertConfigForm.vue';
import AlertConfigForm from '../modules/alert-config-form.vue';
import { useGridColumns, useGridFormSchema } from './data';
defineOptions({ name: 'IoTAlertConfig' });

View File

@@ -32,10 +32,10 @@ import { getSimpleProductList } from '#/api/iot/product/product';
import { $t } from '#/locales';
import { useGridColumns } from './data';
import DeviceCardView from './modules/DeviceCardView.vue';
import DeviceForm from './modules/DeviceForm.vue';
import DeviceGroupForm from './modules/DeviceGroupForm.vue';
import DeviceImportForm from './modules/DeviceImportForm.vue';
import DeviceCardView from './modules/device-card-view.vue';
import DeviceForm from './modules/device-form.vue';
import DeviceGroupForm from './modules/device-group-form.vue';
import DeviceImportForm from './modules/device-import-form.vue';
/** IoT 设备列表 */
defineOptions({ name: 'IoTDevice' });
@@ -48,6 +48,7 @@ const viewMode = ref<'card' | 'list'>('card');
const cardViewRef = ref();
// Modal instances
// TODO @haohao这个界面等 product 改完,在一起看看怎么弄更好。
const [DeviceFormModal, deviceFormModalApi] = useVbenModal({
connectedComponent: DeviceForm,
destroyOnClose: true,

View File

@@ -8,7 +8,7 @@ import { useRouter } from 'vue-router';
import { Button, Card, Descriptions, message } from 'ant-design-vue';
import DeviceForm from '../DeviceForm.vue';
import DeviceForm from '../device-form.vue';
interface Props {
product: IotProductApi.Product;

View File

@@ -22,13 +22,13 @@ import {
} from 'ant-design-vue';
import { DeviceStateEnum, sendDeviceMessage } from '#/api/iot/device/device';
import DataDefinition from '#/views/iot/thingmodel/modules/components/DataDefinition.vue';
import DataDefinition from '#/views/iot/thingmodel/modules/components/data-definition.vue';
import {
IotDeviceMessageMethodEnum,
IoTThingModelTypeEnum,
} from '#/views/iot/utils/constants';
import DeviceDetailsMessage from './DeviceDetailsMessage.vue';
import DeviceDetailsMessage from './device-details-message.vue';
const props = defineProps<{
device: IotDeviceApi.Device;

View File

@@ -354,7 +354,7 @@ async function handleExport() {
...list.value.map((item, index) => {
return [
index + 1,
formatDate(new Date(item.updateTime)),
formatDateTime(new Date(item.updateTime)),
isComplexDataType.value
? `"${JSON.stringify(item.value)}"`
: item.value,
@@ -553,15 +553,17 @@ defineExpose({ open }); // 提供 open 方法,用于打开弹窗
.toolbar-wrapper {
padding: 16px;
background-color: #fafafa;
background-color: hsl(var(--card) / 0.9);
border-radius: 8px;
border: 1px solid hsl(var(--border) / 0.6);
}
.chart-container,
.table-container {
padding: 16px;
background-color: #fff;
background-color: hsl(var(--card));
border-radius: 8px;
border: 1px solid hsl(var(--border) / 0.6);
}
}
</style>

View File

@@ -22,7 +22,8 @@ import {
import { getLatestDeviceProperties } from '#/api/iot/device/device';
import DeviceDetailsThingModelPropertyHistory from './DeviceDetailsThingModelPropertyHistory.vue';
import DeviceDetailsThingModelPropertyHistory
from './device-details-thing-model-property-history.vue';
const props = defineProps<{ deviceId: number }>();
@@ -123,10 +124,12 @@ onMounted(() => {
style="width: 240px"
@press-enter="handleQuery"
/>
<div class="flex items-center" style="gap: 8px">
<span style="font-size: 14px; color: #666">自动刷新</span>
<Switch v-model:checked="autoRefresh" size="small" />
</div>
<Switch
v-model:checked="autoRefresh"
class="ml-20px"
checked-children="定时刷新"
un-checked-children="定时刷新"
/>
</div>
<Button.Group>
<Button

View File

@@ -8,9 +8,9 @@ import { ContentWrap } from '@vben/common-ui';
import { Tabs } from 'ant-design-vue';
import DeviceDetailsThingModelEvent from './DeviceDetailsThingModelEvent.vue';
import DeviceDetailsThingModelProperty from './DeviceDetailsThingModelProperty.vue';
import DeviceDetailsThingModelService from './DeviceDetailsThingModelService.vue';
import DeviceDetailsThingModelEvent from './device-details-thing-model-event.vue';
import DeviceDetailsThingModelProperty from './device-details-thing-model-property.vue';
import DeviceDetailsThingModelService from './device-details-thing-model-service.vue';
const props = defineProps<{
deviceId: number;

View File

@@ -14,13 +14,13 @@ import { getDevice } from '#/api/iot/device/device';
import { DeviceTypeEnum, getProduct } from '#/api/iot/product/product';
import { getThingModelListByProductId } from '#/api/iot/thingmodel';
import DeviceDetailConfig from './DeviceDetailConfig.vue';
import DeviceDetailsHeader from './DeviceDetailsHeader.vue';
import DeviceDetailsInfo from './DeviceDetailsInfo.vue';
import DeviceDetailsMessage from './DeviceDetailsMessage.vue';
import DeviceDetailsSimulator from './DeviceDetailsSimulator.vue';
import DeviceDetailsSubDevice from './DeviceDetailsSubDevice.vue';
import DeviceDetailsThingModel from './DeviceDetailsThingModel.vue';
import DeviceDetailConfig from './device-detail-config.vue';
import DeviceDetailsHeader from './device-details-header.vue';
import DeviceDetailsInfo from './device-details-info.vue';
import DeviceDetailsMessage from './device-details-message.vue';
import DeviceDetailsSimulator from './device-details-simulator.vue';
import DeviceDetailsSubDevice from './device-details-sub-device.vue';
import DeviceDetailsThingModel from './device-details-thing-model.vue';
defineOptions({ name: 'IoTDeviceDetail' });

View File

@@ -2,8 +2,9 @@
import { onMounted, ref } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictLabel } from '@vben/hooks';
import { getDictLabel, getDictObj } from '@vben/hooks';
import { IconifyIcon } from '@vben/icons';
import { isValidColor, TinyColor } from '@vben/utils';
import {
Button,
@@ -52,6 +53,91 @@ const queryParams = ref({
pageSize: 12,
});
const DEFAULT_STATUS_MAP: Record<
'default' | number,
{ bgColor: string; borderColor: string; color: string; text: string }
> = {
[DeviceStateEnum.ONLINE]: {
text: '在线',
color: '#52c41a',
bgColor: '#f6ffed',
borderColor: '#b7eb8f',
},
[DeviceStateEnum.OFFLINE]: {
text: '离线',
color: '#faad14',
bgColor: '#fffbe6',
borderColor: '#ffe58f',
},
[DeviceStateEnum.INACTIVE]: {
text: '未激活',
color: '#ff4d4f',
bgColor: '#fff1f0',
borderColor: '#ffccc7',
},
default: {
text: '未知状态',
color: '#595959',
bgColor: '#fafafa',
borderColor: '#d9d9d9',
},
};
const COLOR_TYPE_PRESETS: Record<
string,
{ bgColor: string; borderColor: string; color: string }
> = {
success: {
color: '#52c41a',
bgColor: '#f6ffed',
borderColor: '#b7eb8f',
},
processing: {
color: '#1890ff',
bgColor: '#e6f7ff',
borderColor: '#91d5ff',
},
warning: {
color: '#faad14',
bgColor: '#fffbe6',
borderColor: '#ffe58f',
},
error: {
color: '#ff4d4f',
bgColor: '#fff1f0',
borderColor: '#ffccc7',
},
default: {
color: '#595959',
bgColor: '#fafafa',
borderColor: '#d9d9d9',
},
};
function normalizeColorType(colorType?: string) {
switch (colorType) {
case 'danger': {
return 'error';
}
case 'default':
case 'error':
case 'processing':
case 'success':
case 'warning': {
return colorType;
}
case 'info': {
return 'default';
}
case 'primary': {
return 'processing';
}
default: {
return 'default';
}
}
}
//
function getProductName(productId: number) {
const product = props.products.find((p: any) => p.id === productId);
@@ -90,21 +176,41 @@ function getDeviceTypeColor(deviceType: number) {
}
//
function getStatusInfo(state: number) {
if (state === DeviceStateEnum.ONLINE) {
function getStatusInfo(state: number | string | null | undefined) {
const parsedState = Number(state);
const hasNumericState = Number.isFinite(parsedState);
const fallback = hasNumericState
? DEFAULT_STATUS_MAP[parsedState] || DEFAULT_STATUS_MAP.default
: DEFAULT_STATUS_MAP.default;
const dict = getDictObj(
DICT_TYPE.IOT_DEVICE_STATE,
hasNumericState ? parsedState : state,
);
if (dict) {
if (!dict.colorType && !dict.cssClass) {
return {
...fallback,
text: dict.label || fallback.text,
};
}
const presetKey = normalizeColorType(dict.colorType);
if (isValidColor(dict.cssClass)) {
const baseColor = new TinyColor(dict.cssClass);
return {
text: dict.label || fallback.text,
color: baseColor.toHexString(),
bgColor: baseColor.clone().setAlpha(0.15).toRgbString(),
borderColor: baseColor.clone().lighten(30).toHexString(),
};
}
const preset = COLOR_TYPE_PRESETS[presetKey] || COLOR_TYPE_PRESETS.default;
return {
text: '在线',
color: '#52c41a',
bgColor: '#f6ffed',
borderColor: '#b7eb8f',
text: dict.label || fallback.text,
...preset,
};
}
return {
text: '未激活',
color: '#ff4d4f',
bgColor: '#fff1f0',
borderColor: '#ffccc7',
};
return fallback;
}
onMounted(() => {
@@ -290,21 +396,21 @@ defineExpose({
.device-card {
height: 100%;
overflow: hidden;
background: #fff;
border: 1px solid #f0f0f0;
background: hsl(var(--card) / 0.95);
border: 1px solid hsl(var(--border) / 0.6);
border-radius: 8px;
box-shadow:
0 1px 2px 0 rgb(0 0 0 / 3%),
0 1px 6px -1px rgb(0 0 0 / 2%),
0 2px 4px 0 rgb(0 0 0 / 2%);
0 1px 2px 0 hsl(var(--foreground) / 0.04),
0 1px 6px -1px hsl(var(--foreground) / 0.05),
0 2px 4px 0 hsl(var(--foreground) / 0.05);
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
&:hover {
border-color: #e6e6e6;
border-color: hsl(var(--border));
box-shadow:
0 1px 2px -2px rgb(0 0 0 / 16%),
0 3px 6px 0 rgb(0 0 0 / 12%),
0 5px 12px 4px rgb(0 0 0 / 9%);
0 1px 2px -2px hsl(var(--foreground) / 0.12),
0 3px 6px 0 hsl(var(--foreground) / 0.1),
0 5px 12px 4px hsl(var(--foreground) / 0.08);
transform: translateY(-4px);
}
@@ -367,7 +473,7 @@ defineExpose({
font-size: 16px;
font-weight: 600;
line-height: 24px;
color: #262626;
color: hsl(var(--foreground) / 0.9);
white-space: nowrap;
}
@@ -390,7 +496,7 @@ defineExpose({
.label {
flex-shrink: 0;
font-size: 13px;
color: #8c8c8c;
color: hsl(var(--foreground) / 0.6);
}
.value {
@@ -399,17 +505,17 @@ defineExpose({
overflow: hidden;
text-overflow: ellipsis;
font-size: 13px;
color: #262626;
color: hsl(var(--foreground) / 0.85);
text-align: right;
white-space: nowrap;
&.link {
color: #1890ff;
color: hsl(var(--primary));
cursor: pointer;
transition: color 0.2s;
&:hover {
color: #40a9ff;
color: hsl(var(--primary) / 0.85);
}
}
@@ -418,7 +524,7 @@ defineExpose({
'SF Mono', Monaco, Inconsolata, 'Fira Code', Consolas, monospace;
font-size: 12px;
font-weight: 500;
color: #595959;
color: hsl(var(--foreground) / 0.6);
}
}
}
@@ -431,7 +537,7 @@ defineExpose({
display: flex;
gap: 8px;
padding-top: 12px;
border-top: 1px solid #f5f5f5;
border-top: 1px solid hsl(var(--border) / 0.4);
.action-btn {
display: flex;
@@ -445,7 +551,7 @@ defineExpose({
font-weight: 400;
pointer-events: auto;
cursor: pointer;
border: 1px solid;
border: 1px solid transparent;
border-radius: 6px;
transition: all 0.2s;
@@ -454,52 +560,60 @@ defineExpose({
}
&.btn-edit {
color: #1890ff;
background: #e6f7ff;
border-color: #91d5ff;
color: hsl(var(--primary));
background: hsl(var(--primary) / 0.12);
border-color: hsl(var(--primary) / 0.25);
&:hover {
color: #fff;
background: #1890ff;
border-color: #1890ff;
color: hsl(var(--primary-foreground));
background: hsl(var(--primary));
border-color: hsl(var(--primary));
}
}
&.btn-view {
color: #faad14;
background: #fffbe6;
border-color: #ffe58f;
color: hsl(var(--warning));
background: hsl(var(--warning) / 0.12);
border-color: hsl(var(--warning) / 0.25);
&:hover {
color: #fff;
background: #faad14;
border-color: #faad14;
background: hsl(var(--warning));
border-color: hsl(var(--warning));
}
}
&.btn-data {
color: #722ed1;
background: #f9f0ff;
border-color: #d3adf7;
color: hsl(var(--accent-foreground));
background: color-mix(
in srgb,
hsl(var(--accent)) 40%,
hsl(var(--card)) 60%
);
border-color: color-mix(
in srgb,
hsl(var(--accent)) 55%,
transparent
);
&:hover {
color: #fff;
background: #722ed1;
border-color: #722ed1;
color: hsl(var(--accent-foreground));
background: hsl(var(--accent));
border-color: hsl(var(--accent));
}
}
&.btn-delete {
flex: 0 0 32px;
padding: 4px;
color: #ff4d4f;
background: #fff1f0;
border-color: #ffa39e;
color: hsl(var(--destructive));
background: hsl(var(--destructive) / 0.12);
border-color: hsl(var(--destructive) / 0.3);
&:hover {
color: #fff;
background: #ff4d4f;
border-color: #ff4d4f;
color: hsl(var(--destructive-foreground));
background: hsl(var(--destructive));
border-color: hsl(var(--destructive));
}
}
}

View File

@@ -43,9 +43,7 @@ async function handleDelete(row: IotDeviceGroupApi.DeviceGroup) {
});
try {
await deleteDeviceGroup(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
});
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
handleRefresh();
} finally {
hideLoading();
@@ -55,7 +53,6 @@ async function handleDelete(row: IotDeviceGroupApi.DeviceGroup) {
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
showCollapseButton: true,
},
gridOptions: {
columns: useGridColumns(),

View File

@@ -19,6 +19,8 @@ import { useFormSchema } from '../data';
defineOptions({ name: 'IoTDeviceGroupForm' });
// TODO @haohaoweb-antd/src/views/iot/product/category/modules/product-category-form.vue 类似问题
const emit = defineEmits<{
success: [];
}>();

View File

@@ -1,6 +1,5 @@
/**
*
*/
/** 设备数量饼图配置 */
// TODO @haohao貌似没用到
export function getDeviceCountChartOptions(
productCategoryDeviceCounts: Record<string, number>,
): any {

View File

@@ -101,6 +101,7 @@ export function useIotHome() {
};
}
// TODO @haohao是不是删除下哈
/** 格式化数字 - 大数字显示为 K/M */
export function formatNumber(num: number): string {
if (num >= 1_000_000) {

View File

@@ -3,17 +3,15 @@ import { Page } from '@vben/common-ui';
import { Col, Row } from 'ant-design-vue';
// 导入业务逻辑
import { useIotHome } from './data';
// 导入组件
import ComparisonCard from './modules/ComparisonCard.vue';
import DeviceCountCard from './modules/DeviceCountCard.vue';
import DeviceStateCountCard from './modules/DeviceStateCountCard.vue';
import MessageTrendCard from './modules/MessageTrendCard.vue';
import ComparisonCard from './modules/comparison-card.vue';
import DeviceCountCard from './modules/device-count-card.vue';
import DeviceStateCountCard from './modules/device-state-count-card.vue';
import MessageTrendCard from './modules/message-trend-card.vue';
defineOptions({ name: 'IoTHome' });
// 使用业务逻辑 Hook
// TODO @haohao相关的方法拿到 index.vue 里data.ts 只放 schema
const { loading, statsData } = useIotHome();
</script>

View File

@@ -6,6 +6,8 @@ import { createIconifyIcon } from '@vben/icons';
import { Card } from 'ant-design-vue';
// TODO @haohao packages/effects/common-ui/src/components/card/comparison-card
defineOptions({ name: 'ComparisonCard' });
const props = defineProps<{
@@ -59,6 +61,7 @@ const IconComponent = computed(() => iconMap[props.icon] || iconMap.menu);
</template>
<style scoped>
/** TODO tindwind */
.stat-card {
height: 160px;
cursor: pointer;

View File

@@ -28,13 +28,17 @@ const hasData = computed(() => {
/** 初始化图表 */
function initChart() {
if (!hasData.value) return;
if (!hasData.value) {
return;
}
// TODO @haohaoawait nextTick();
nextTick(() => {
const data = Object.entries(
props.statsData.productCategoryDeviceCounts,
).map(([name, value]) => ({ name, value }));
// TODO @haohao chart-options apps/web-antd/src/views/mall/statistics/member/modules/area-chart-options.ts
renderEcharts({
tooltip: {
trigger: 'item',
@@ -132,6 +136,7 @@ onMounted(() => {
</template>
<style scoped>
/** TODO tindwind */
.chart-card {
height: 100%;
}

View File

@@ -31,6 +31,7 @@ const hasData = computed(() => {
});
/** 获取仪表盘配置 */
// TODO @haohao chart-options apps/web-antd/src/views/mall/statistics/member/modules/area-chart-options.ts
const getGaugeOption = (value: number, color: string, title: string): any => {
return {
series: [
@@ -81,8 +82,11 @@ const getGaugeOption = (value: number, color: string, title: string): any => {
/** 初始化图表 */
function initCharts() {
if (!hasData.value) return;
if (!hasData.value) {
return;
}
// TODO @haohaoawait nextTick();
nextTick(() => {
// 线
renderOnlineChart(
@@ -147,6 +151,7 @@ onMounted(() => {
</template>
<style scoped>
/** TODO tindwind */
.chart-card {
height: 100%;
}

View File

@@ -24,6 +24,8 @@ const messageData = ref<IotStatisticsApi.DeviceMessageSummaryByDate[]>([]);
const activeTimeRange = ref('7d'); //
const dateRange = ref<[Dayjs, Dayjs] | undefined>(undefined);
// TODO @haohao
const queryParams = reactive<IotStatisticsApi.DeviceMessageReq>({
interval: 1, //
times: [],
@@ -34,6 +36,7 @@ const hasData = computed(() => {
return messageData.value && messageData.value.length > 0;
});
// TODO @haohao /** */ //
//
function setTimeRange(range: string) {
activeTimeRange.value = range;
@@ -64,6 +67,7 @@ function setTimeRange(range: string) {
}
}
// TODO @haohao使 formatDateTime
queryParams.times = [
start.format('YYYY-MM-DD HH:mm:ss'),
end.format('YYYY-MM-DD HH:mm:ss'),
@@ -78,6 +82,7 @@ function handleDateChange() {
activeTimeRange.value = ''; //
queryParams.interval = 1; //
queryParams.times = [
// TODO @haohao使 formatDateTime
dateRange.value[0].startOf('day').format('YYYY-MM-DD HH:mm:ss'),
dateRange.value[1].endOf('day').format('YYYY-MM-DD HH:mm:ss'),
];
@@ -110,6 +115,7 @@ function initChart() {
const upstreamData = messageData.value.map((item) => item.upstreamCount);
const downstreamData = messageData.value.map((item) => item.downstreamCount);
// TODO @haohao chart-options apps/web-antd/src/views/mall/statistics/member/modules/area-chart-options.ts
renderEcharts({
tooltip: {
trigger: 'axis',

View File

@@ -13,7 +13,7 @@ import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteOtaFirmware, getOtaFirmwarePage } from '#/api/iot/ota/firmware';
import { $t } from '#/locales';
import Form from '../modules/OtaFirmwareForm.vue';
import OtaFirmwareForm from '../modules/ota-firmware-form.vue';
import { useGridColumns, useGridFormSchema } from './data';
defineOptions({ name: 'IoTOtaFirmware' });
@@ -21,7 +21,7 @@ defineOptions({ name: 'IoTOtaFirmware' });
const { push } = useRouter();
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
connectedComponent: OtaFirmwareForm,
destroyOnClose: true,
});

View File

@@ -11,7 +11,7 @@ import { deleteOtaFirmware, getOtaFirmwarePage } from '#/api/iot/ota/firmware';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import OtaFirmwareForm from './modules/OtaFirmwareForm.vue';
import OtaFirmwareForm from './modules/ota-firmware-form.vue';
defineOptions({ name: 'IoTOtaFirmware' });

View File

@@ -12,7 +12,7 @@ import { getOtaFirmware } from '#/api/iot/ota/firmware';
import { getOtaTaskRecordStatusStatistics } from '#/api/iot/ota/task/record';
import { IoTOtaTaskRecordStatusEnum } from '#/views/iot/utils/constants';
import OtaTaskList from '../task/OtaTaskList.vue';
import OtaTaskList from '../task/ota-task-list.vue';
/** IoT OTA 固件详情 */
defineOptions({ name: 'IoTOtaFirmwareDetail' });

View File

@@ -12,7 +12,7 @@ import { getOtaFirmware } from '#/api/iot/ota/firmware';
import { getOtaTaskRecordStatusStatistics } from '#/api/iot/ota/task/record';
import { IoTOtaTaskRecordStatusEnum } from '#/views/iot/utils/constants';
import OtaTaskList from '../task/OtaTaskList.vue';
import OtaTaskList from '../task/ota-task-list.vue';
/** IoT OTA 固件详情 */
defineOptions({ name: 'IoTOtaFirmwareDetail' });

View File

@@ -22,8 +22,8 @@ import {
import { getOtaTaskPage } from '#/api/iot/ota/task';
import { IoTOtaTaskStatusEnum } from '#/views/iot/utils/constants';
import OtaTaskDetail from './OtaTaskDetail.vue';
import OtaTaskForm from './OtaTaskForm.vue';
import OtaTaskDetail from './ota-task-detail.vue';
import OtaTaskForm from './ota-task-form.vue';
/** IoT OTA 任务列表 */
defineOptions({ name: 'OtaTaskList' });

View File

@@ -1,31 +0,0 @@
<script lang="ts" setup>
import { Page } from '@vben/common-ui';
import { Button } from 'ant-design-vue';
defineOptions({ name: 'IotPlugin' });
</script>
<template>
<Page description="物聯網插件管理" title="插件管理">
<div class="p-4">
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
>
該功能支持 Vue3 + element-plus 版本
</Button>
<div class="mt-4">
<h3>功能說明</h3>
<p>IoT 插件管理功能</p>
<h3 class="mt-4">待實現</h3>
<ul>
<li> API 接口定義</li>
<li> 頁面實現</li>
</ul>
</div>
</div>
</Page>
</template>

View File

@@ -14,12 +14,12 @@ import {
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/ProductCategoryForm.vue';
import ProductCategoryForm from './modules/product-category-form.vue';
defineOptions({ name: 'IoTProductCategory' });
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
connectedComponent: ProductCategoryForm,
destroyOnClose: true,
});
@@ -56,16 +56,11 @@ async function handleDelete(row: IotProductCategoryApi.ProductCategory) {
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
showCollapseButton: true,
collapsed: true,
},
gridOptions: {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
pagerConfig: {
enabled: true,
},
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {

View File

@@ -17,6 +17,8 @@ import { $t } from '#/locales';
import { useFormSchema } from '../data';
// TODO @haohao form.vue
const emit = defineEmits(['success']);
const formData = ref<IotProductCategoryApi.ProductCategory>();
const getTitle = computed(() => {
@@ -38,7 +40,7 @@ const [Form, formApi] = useVbenForm({
showDefaultActions: false,
});
// TODO @haohao form12
// TODO @haohao apps/web-antd/src/views/system/dept/modules/form.vue useVbenModal
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();

View File

@@ -23,7 +23,6 @@ export function useFormSchema(formApi?: any): VbenFormSchema[] {
show: () => false,
},
},
// 创建时的 ProductKey 字段(带生成按钮)
{
fieldName: 'productKey',
label: 'ProductKey',
@@ -34,7 +33,6 @@ export function useFormSchema(formApi?: any): VbenFormSchema[] {
dependencies: {
triggerFields: ['id'],
if(values) {
// 仅在创建时显示(没有 id
return !values.id;
},
},
@@ -43,6 +41,7 @@ export function useFormSchema(formApi?: any): VbenFormSchema[] {
.min(1, 'ProductKey 不能为空')
.max(32, 'ProductKey 长度不能超过 32 个字符'),
suffix: () => {
// 创建时的 ProductKey 字段(带生成按钮)
return h(
Button,
{
@@ -55,19 +54,17 @@ export function useFormSchema(formApi?: any): VbenFormSchema[] {
);
},
},
// 编辑时的 ProductKey 字段(禁用,无按钮)
{
fieldName: 'productKey',
label: 'ProductKey',
component: 'Input',
componentProps: {
placeholder: '请输入 ProductKey',
disabled: true,
disabled: true, // 编辑时的 ProductKey 字段(禁用,无按钮)
},
dependencies: {
triggerFields: ['id'],
if(values) {
// 仅在编辑时显示(有 id
return !!values.id;
},
},
@@ -176,7 +173,6 @@ export function useBasicFormSchema(formApi?: any): VbenFormSchema[] {
show: () => false,
},
},
// 创建时的 ProductKey 字段(带生成按钮)
{
fieldName: 'productKey',
label: 'ProductKey',
@@ -187,7 +183,6 @@ export function useBasicFormSchema(formApi?: any): VbenFormSchema[] {
dependencies: {
triggerFields: ['id'],
if(values) {
// 仅在创建时显示(没有 id
return !values.id;
},
},
@@ -208,7 +203,6 @@ export function useBasicFormSchema(formApi?: any): VbenFormSchema[] {
);
},
},
// 编辑时的 ProductKey 字段(禁用,无按钮)
{
fieldName: 'productKey',
label: 'ProductKey',
@@ -220,7 +214,6 @@ export function useBasicFormSchema(formApi?: any): VbenFormSchema[] {
dependencies: {
triggerFields: ['id'],
if(values) {
// 仅在编辑时显示(有 id
return !!values.id;
},
},
@@ -341,6 +334,7 @@ export function useAdvancedFormSchema(): VbenFormSchema[] {
}
/** 列表的搜索表单 */
// TODO @haohao貌似用不上
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
@@ -367,7 +361,6 @@ export function useGridFormSchema(): VbenFormSchema[] {
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions['columns'] {
return [
{ type: 'checkbox', width: 40 },
{
field: 'id',
title: 'ID',
@@ -413,7 +406,7 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
},
{
title: '操作',
width: 180,
width: 220,
fixed: 'right',
slots: { default: 'actions' },
},
@@ -421,6 +414,7 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
}
/** 查询产品列表 */
// TODO @haohao貌似可以删除
export async function queryProductList({ page }: any, searchParams: any) {
return await getProductPage({
pageNo: page.currentPage,
@@ -430,6 +424,7 @@ export async function queryProductList({ page }: any, searchParams: any) {
}
/** 创建图片预览状态 */
// TODO @haohao可能不一定用的上
export function useImagePreview() {
const previewVisible = ref(false);
const previewImage = ref('');
@@ -446,6 +441,7 @@ export function useImagePreview() {
};
}
// TODO @haohao放到对应的 form 里
/** 生成 ProductKey包含大小写字母和数字 */
export function generateProductKey(): string {
const chars =

View File

@@ -20,23 +20,20 @@ import {
import { $t } from '#/locales';
import { useGridColumns, useImagePreview } from './data';
import ProductCardView from './modules/ProductCardView.vue';
import ProductForm from './modules/ProductForm.vue';
import ProductCardView from './modules/product-card-view.vue';
import ProductForm from './modules/product-form.vue';
defineOptions({ name: 'IoTProduct' });
const router = useRouter();
const categoryList = ref<any[]>([]);
const categoryList = ref<any[]>([]); // TODO @haohaocategory 类型
const viewMode = ref<'card' | 'list'>('card');
const cardViewRef = ref();
// 搜索参数
const searchParams = ref({
name: '',
productKey: '',
});
}); // 搜索参数
// 图片预览
const { previewVisible, previewImage, handlePreviewImage } = useImagePreview();
const [FormModal, formModalApi] = useVbenModal({
@@ -44,18 +41,19 @@ const [FormModal, formModalApi] = useVbenModal({
destroyOnClose: true,
});
// 加载产品分类列表
/** 加载产品分类列表 */
async function loadCategories() {
categoryList.value = await getSimpleProductCategoryList();
}
// 获取分类名称
/** 获取分类名称 */
function getCategoryNameByValue(categoryId: number) {
const category = categoryList.value.find((c: any) => c.id === categoryId);
return category?.name || '未分类';
}
/** 搜索 */
// TODO @haohao要不要改成 handleRefresh注释改成“刷新表格”更加统一。
/** 搜索产品 */
function handleSearch() {
if (viewMode.value === 'list') {
gridApi.formApi.setValues(searchParams.value);
@@ -65,14 +63,14 @@ function handleSearch() {
}
}
/** 重置 */
/** 重置搜索 */
function handleReset() {
searchParams.value.name = '';
searchParams.value.productKey = '';
handleSearch();
}
/** 刷新 */
/** 刷新表格 */
function handleRefresh() {
if (viewMode.value === 'list') {
gridApi.query();
@@ -84,7 +82,7 @@ function handleRefresh() {
/** 导出表格 */
async function handleExport() {
const data = await exportProduct(searchParams.value);
await downloadFileFromBlobPart({ fileName: '产品列表.xls', source: data });
downloadFileFromBlobPart({ fileName: '产品列表.xls', source: data });
}
/** 打开产品详情 */
@@ -117,12 +115,12 @@ function handleEdit(row: any) {
/** 删除产品 */
async function handleDelete(row: any) {
const hideLoading = message.loading({
content: `正在删除 ${row.name}...`,
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
});
try {
await deleteProduct(row.id);
message.success(`删除 ${row.name} 成功`);
await deleteProduct(row.id!);
message.success($t('ui.actionMessage.deleteSuccess'));
handleRefresh();
} finally {
hideLoading();
@@ -130,6 +128,7 @@ async function handleDelete(row: any) {
}
const [Grid, gridApi] = useVbenVxeGrid({
// TODO @haohao这个不用可以删除掉的
formOptions: {
schema: [],
},
@@ -156,9 +155,10 @@ const [Grid, gridApi] = useVbenVxeGrid({
refresh: true,
search: true,
},
} as VxeTableGridOptions,
} as VxeTableGridOptions, // TODO @haohao这里有个 <> 泛型
});
/** 初始化 */
onMounted(() => {
loadCategories();
});
@@ -172,22 +172,24 @@ onMounted(() => {
<Card :body-style="{ padding: '16px' }" class="mb-4">
<!-- 搜索表单 -->
<div class="mb-3 flex items-center gap-3">
<!-- TODO @haohaotindwind -->
<Input
v-model:value="searchParams.name"
placeholder="请输入产品名称"
allow-clear
style="width: 200px"
style="width: 220px"
@press-enter="handleSearch"
>
<template #prefix>
<span class="text-gray-400">产品名称</span>
</template>
</Input>
<!-- TODO @haohaotindwind -->
<Input
v-model:value="searchParams.productKey"
placeholder="请输入产品标识"
allow-clear
style="width: 200px"
style="width: 220px"
@press-enter="handleSearch"
>
<template #prefix>
@@ -203,20 +205,20 @@ onMounted(() => {
重置
</Button>
</div>
<!-- 操作按钮 -->
<div class="flex items-center justify-between">
<Space :size="12">
<Button type="primary" @click="handleCreate">
<!-- TODO @haohao按钮使用中立的ACTION_ICON.ADD -->
<IconifyIcon icon="ant-design:plus-outlined" class="mr-1" />
新增产品
</Button>
<Button type="primary" @click="handleExport">
<!-- TODO @haohao按钮使用中立的ACTION_ICON.EXPORT -->
<IconifyIcon icon="ant-design:download-outlined" class="mr-1" />
导出
</Button>
</Space>
<!-- 视图切换 -->
<Space :size="4">
<Button
@@ -236,16 +238,17 @@ onMounted(() => {
</Card>
<Grid v-show="viewMode === 'list'">
<!-- TODO @haohao这里貌似可以删除掉 -->
<template #toolbar-tools>
<div></div>
</template>
<!-- 产品分类列 -->
<!-- TODO @haohao这里应该可以拿到 data.ts参考别的模块类似 apps/web-antd/src/views/ai/image/manager/data.ts 里面查询 category 和自己渲染-->
<template #category="{ row }">
<span>{{ getCategoryNameByValue(row.categoryId) }}</span>
</template>
<!-- 产品图标列 -->
<!-- TODO @haohao直接用 Image 组件 ok 了呀 data.ts -->
<template #icon="{ row }">
<Button
v-if="row.icon"
@@ -257,7 +260,7 @@ onMounted(() => {
</Button>
<span v-else class="text-gray-400">-</span>
</template>
<!-- TODO @haohao直接用 Image 组件 ok 了呀 data.ts -->
<!-- 产品图片列 -->
<template #picUrl="{ row }">
<Button
@@ -270,8 +273,6 @@ onMounted(() => {
</Button>
<span v-else class="text-gray-400">-</span>
</template>
<!-- 操作列 -->
<template #actions="{ row }">
<TableAction
:actions="[
@@ -320,7 +321,9 @@ onMounted(() => {
/>
<!-- 图片预览 -->
<!-- TODO @haohaotindwind -->
<div style="display: none">
<!-- TODO @haohao是不是通过 Image 直接实现预览 -->
<Image.PreviewGroup
:preview="{
visible: previewVisible,
@@ -333,6 +336,7 @@ onMounted(() => {
</Page>
</template>
<style scoped>
/** TODO @haohao貌似这 2 个 css 没啥用? */
:deep(.vxe-toolbar div) {
z-index: 1;
}

View File

@@ -1,5 +1,6 @@
<!-- IoT 产品选择器使用弹窗展示 -->
<script setup lang="ts">
// TODO @haohao
import type { IotProductApi } from '#/api/iot/product/product';
import { reactive, ref } from 'vue';
@@ -28,6 +29,7 @@ interface Props {
const [Modal, modalApi] = useVbenModal({
title: '产品选择器',
// TODO @haohaohandleConfirm
onConfirm: handleConfirm,
});
@@ -39,6 +41,7 @@ const queryParams = reactive({
name: '',
productKey: '',
});
// TODO @haohao form Grid
//
const [Grid, gridApi] = useVbenVxeGrid({

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import type { IotProductApi } from '#/api/iot/product/product';
// TODO @haohaodetail 挪到 yudao-ui-admin-vben-v5/apps/web-antd/src/views/iot/product/product/detail 下。独立一个,不放在 modules 里。
import { onMounted, provide, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
@@ -12,8 +13,8 @@ import { getDeviceCount } from '#/api/iot/device/device';
import { getProduct } from '#/api/iot/product/product';
import IoTProductThingModel from '#/views/iot/thingmodel/index.vue';
import ProductDetailsHeader from './ProductDetailsHeader.vue';
import ProductDetailsInfo from './ProductDetailsInfo.vue';
import ProductDetailsHeader from './product-details-header.vue';
import ProductDetailsInfo from './product-details-info.vue';
defineOptions({ name: 'IoTProductDetail' });
@@ -25,8 +26,7 @@ const loading = ref(true);
const product = ref<IotProductApi.Product>({} as IotProductApi.Product);
const activeTab = ref('info');
// 提供产品信息给子组件
provide('product', product);
provide('product', product); // 提供产品信息给子组件
/** 获取产品详情 */
async function getProductData(productId: number) {
@@ -44,13 +44,8 @@ async function getProductData(productId: number) {
async function getDeviceCountData(productId: number) {
try {
return await getDeviceCount(productId);
} catch (error) {
console.error(
'Error fetching device count:',
error,
'productId:',
productId,
);
} catch {
message.error('获取设备数量失败');
return 0;
}
}
@@ -62,7 +57,6 @@ onMounted(async () => {
router.back();
return;
}
await getProductData(id);
// 处理 tab 参数
@@ -70,7 +64,6 @@ onMounted(async () => {
if (tab) {
activeTab.value = tab as string;
}
// 查询设备数量
if (product.value.id) {
product.value.deviceCount = await getDeviceCountData(product.value.id);
@@ -85,7 +78,6 @@ onMounted(async () => {
:product="product"
@refresh="() => getProductData(id)"
/>
<Tabs v-model:active-key="activeTab" class="mt-4">
<Tabs.TabPane key="info" tab="产品信息">
<ProductDetailsInfo v-if="activeTab === 'info'" :product="product" />

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
// TODO @haohao detail/modules header.vue
import type { IotProductApi } from '#/api/iot/product/product';
import { ref } from 'vue';
@@ -8,7 +9,7 @@ import { Button, Card, Descriptions, message } from 'ant-design-vue';
import { updateProductStatus } from '#/api/iot/product/product';
import ProductForm from '../ProductForm.vue';
import ProductForm from '../product-form.vue';
interface Props {
product: IotProductApi.Product;
@@ -51,8 +52,9 @@ function openForm(type: string, id?: number) {
/** 发布产品 */
async function confirmPublish(id: number) {
// TODO @haohaoasync function handleDeleteBatch() { 1 confirm2 loading
try {
await updateProductStatus(id, 1);
await updateProductStatus(id, 1); // TODO @1 0
message.success('发布成功');
emit('refresh');
} catch {
@@ -62,6 +64,7 @@ async function confirmPublish(id: number) {
/** 撤销发布 */
async function confirmUnpublish(id: number) {
// TODO @haohaoasync function handleDeleteBatch() { 1 confirm2 loading
try {
await updateProductStatus(id, 0);
message.success('撤销发布成功');
@@ -126,6 +129,7 @@ async function confirmUnpublish(id: number) {
</Card>
<!-- 表单弹窗 -->
<!-- TODO @haohao弹不出来另外应该用 index.vue Form 的声明方式哈 -->
<ProductForm ref="formRef" @success="emit('refresh')" />
</div>
</template>

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
// TODO @haohao detail/modules info.vue
import type { IotProductApi } from '#/api/iot/product/product';
import { DICT_TYPE } from '@vben/constants';
@@ -23,6 +24,7 @@ function formatDate(date?: Date | string) {
<template>
<Card title="产品信息">
<!-- TODO @haohao看看是不是用 description 组件 -->
<Descriptions bordered :column="3" size="small">
<Descriptions.Item label="产品名称">
{{ product.name }}

View File

@@ -19,6 +19,8 @@ import {
import { getProductPage } from '#/api/iot/product/product';
// TODO @haohao card-view.vue
// TODO @haohao
defineOptions({ name: 'ProductCardView' });
@@ -48,6 +50,7 @@ const queryParams = ref({
pageSize: 12,
});
// TODO @haohao
//
function getCategoryName(categoryId: number) {
const category = props.categoryList.find((c: any) => c.id === categoryId);
@@ -85,11 +88,6 @@ function getDeviceTypeColor(deviceType: number) {
return colors[deviceType] || 'default';
}
onMounted(() => {
getList();
});
//
defineExpose({
reload: getList,
search: () => {
@@ -97,6 +95,11 @@ defineExpose({
getList();
},
});
/** 初始化 */
onMounted(() => {
getList();
});
</script>
<template>
@@ -113,9 +116,11 @@ defineExpose({
:lg="6"
class="mb-4"
>
<!-- TODO @haohao卡片之间的上下距离太宽了 -->
<Card :body-style="{ padding: '20px' }" class="product-card h-full">
<!-- 顶部标题区域 -->
<div class="mb-4 flex items-start">
<!-- TODO @haohao图标太大了看看是不是参考 vue3 + element-plus 搞小点然后标题居中 -->
<div class="product-icon">
<IconifyIcon
:icon="item.icon || 'ant-design:inbox-outlined'"
@@ -126,7 +131,6 @@ defineExpose({
<div class="product-title">{{ item.name }}</div>
</div>
</div>
<!-- 内容区域 -->
<div class="mb-4 flex items-start">
<div class="info-list flex-1">
@@ -152,6 +156,7 @@ defineExpose({
</div>
<div class="info-item">
<span class="info-label">产品标识</span>
<!-- TODO @haohao展示 有点奇怪要不小手 -->
<Tooltip :title="item.productKey || item.id" placement="top">
<span class="info-value product-key">
{{ item.productKey || item.id }}
@@ -159,6 +164,8 @@ defineExpose({
</Tooltip>
</div>
</div>
<!-- TODO @haohao这里是不是有 image然后默认 icon -->
<!-- TODO @haohao高度太高了建议和左侧产品分类 + 产品类型 + 产品标识高度保持一致 -->
<div class="product-3d-icon">
<IconifyIcon
icon="ant-design:box-plot-outlined"
@@ -166,7 +173,6 @@ defineExpose({
/>
</div>
</div>
<!-- 按钮组 -->
<div class="action-buttons">
<Button
@@ -174,6 +180,7 @@ defineExpose({
class="action-btn action-btn-edit"
@click="emit('edit', item)"
>
<!-- TODO @haohao按钮尽量用中立的按钮方便迁移 ele -->
<IconifyIcon icon="ant-design:edit-outlined" class="mr-1" />
编辑
</Button>
@@ -229,13 +236,13 @@ defineExpose({
</Card>
</Col>
</Row>
<!-- 空状态 -->
<Empty v-else description="暂无产品数据" class="my-20" />
</div>
<!-- 分页 -->
<div v-if="list.length > 0" class="mt-6 flex justify-center">
<!-- TODO @haohao放到最右侧好点 -->
<div v-if="list.length > 0" class="flex justify-center">
<Pagination
v-model:current="queryParams.pageNo"
v-model:page-size="queryParams.pageSize"
@@ -251,6 +258,7 @@ defineExpose({
</template>
<style scoped lang="scss">
/** TODO @haohao看看哪些可以 tindwind 掉 */
.product-card-view {
.product-card {
height: 100%;

View File

@@ -20,6 +20,8 @@ import {
useBasicFormSchema,
} from '../data';
// TODO @haohao form.vue
defineOptions({ name: 'IoTProductForm' });
const emit = defineEmits(['success']);
@@ -30,10 +32,9 @@ const formData = ref<any>();
const getTitle = computed(() => {
return formData.value?.id ? '编辑产品' : '新增产品';
});
const activeKey = ref<string[]>([]); // key
// key
const activeKey = ref<string[]>([]);
// TODO @haohao
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
@@ -46,7 +47,7 @@ const [Form, formApi] = useVbenForm({
showDefaultActions: false,
});
//
// TODO @haohao
const [AdvancedForm, advancedFormApi] = useVbenForm({
commonConfig: {
componentProps: {
@@ -59,7 +60,7 @@ const [AdvancedForm, advancedFormApi] = useVbenForm({
showDefaultActions: false,
});
// formApi schema
// TODO @haohao form 61 156
formApi.setState({ schema: useBasicFormSchema(formApi) });
advancedFormApi.setState({ schema: useAdvancedFormSchema() });

View File

@@ -67,12 +67,6 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
title: '规则名称',
minWidth: 150,
},
{
field: 'productId',
title: '所属产品',
minWidth: 150,
slots: { default: 'product' },
},
{
field: 'description',
title: '规则描述',
@@ -87,6 +81,7 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
props: { type: DICT_TYPE.COMMON_STATUS },
},
},
// TODO @haohao这里是【数据源】【数据目的】
{
field: 'sinkCount',
title: '数据流转数',

View File

@@ -1,4 +1,5 @@
<script lang="ts" setup>
// TODO @haohao应该先有【规则】【目的】两个 tab然后在进行管理操作类似apps/web-antd/src/views/ai/chat/manager
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { Page, useVbenModal } from '@vben/common-ui';
@@ -10,7 +11,7 @@ import { deleteDataRule, getDataRulePage } from '#/api/iot/rule/data/rule';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import DataRuleForm from './rule/DataRuleForm.vue';
import DataRuleForm from './rule/data-rule-form.vue';
/** IoT 数据流转规则列表 */
defineOptions({ name: 'IoTDataRule' });
@@ -42,10 +43,8 @@ async function handleDelete(row: any) {
duration: 0,
});
try {
await deleteDataRule(row.id);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
});
await deleteDataRule(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
handleRefresh();
} finally {
hideLoading();

View File

@@ -25,12 +25,11 @@ const formRules: any = reactive({
});
const formRef = ref(); // Ref
//
const upstreamMethods = computed(() => {
return Object.values(IotDeviceMessageMethodEnum).filter(
(item) => item.upstream,
);
});
}); //
/** 根据产品 ID 过滤设备 */
function getFilteredDevices(productId: number) {
@@ -193,7 +192,7 @@ const columns = [
{
title: '操作',
width: 80,
fixed: 'right' as const,
fixed: 'right',
},
];
@@ -202,6 +201,8 @@ defineExpose({ validate, getData, setData });
<template>
<Form ref="formRef" :model="{ data: formData }">
<!-- TODO @haohao貌似有告警 -->
<!-- TODO @haohao是不是搞成 web-antd/src/views/erp/finance/receipt/modules/item-form.vue 的做法通过 Grid apps/web-antd/src/views/infra/demo/demo03/erp/modules/demo03-grade-list.vue目的后续 ele 通用性更好 -->
<Table
:columns="columns"
:data-source="formData"

View File

@@ -14,13 +14,14 @@ import {
import { getDataSinkSimpleList } from '#/api/iot/rule/data/sink';
import { $t } from '#/locales';
import SourceConfigForm from './components/SourceConfigForm.vue';
import SourceConfigForm from './components/source-config-form.vue';
import { useRuleFormSchema } from './data';
const emit = defineEmits(['success']);
const formData = ref<any>();
const sourceConfigRef = ref();
// TODO @haohao modules
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['数据规则'])
@@ -40,6 +41,7 @@ const [Form, formApi] = useVbenForm({
showDefaultActions: false,
});
// TODO @haohao
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();

View File

@@ -10,7 +10,9 @@ import { deleteDataRule, getDataRulePage } from '#/api/iot/rule/data/rule';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import DataRuleForm from './DataRuleForm.vue';
import DataRuleForm from './data-rule-form.vue';
// TODO @haohao貌似和 apps/web-antd/src/views/iot/rule/data/index.vue 重复的。可能这个是对的。然后把 apps/web-antd/src/views/iot/rule/data/index.vue 搞成 tabs
/** IoT 数据流转规则列表 */
defineOptions({ name: 'IotDataRule' });
@@ -43,9 +45,7 @@ async function handleDelete(row: any) {
});
try {
await deleteDataRule(row.id);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
});
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
handleRefresh();
} finally {
hideLoading();

View File

@@ -6,7 +6,7 @@ import { isEmpty } from '@vben/utils';
import { useVModel } from '@vueuse/core';
import { FormItem, Input, Select } from 'ant-design-vue';
import KeyValueEditor from './components/KeyValueEditor.vue';
import KeyValueEditor from './components/key-value-editor.vue';
defineOptions({ name: 'HttpConfigForm' });

View File

@@ -1,6 +1,6 @@
export { default as HttpConfigForm } from './HttpConfigForm.vue';
export { default as KafkaMQConfigForm } from './KafkaMQConfigForm.vue';
export { default as MqttConfigForm } from './MqttConfigForm.vue';
export { default as RabbitMQConfigForm } from './RabbitMQConfigForm.vue';
export { default as RedisStreamConfigForm } from './RedisStreamConfigForm.vue';
export { default as RocketMQConfigForm } from './RocketMQConfigForm.vue';
export {default as HttpConfigForm} from './http-config-form.vue';
export {default as KafkaMqConfigForm} from './kafka-mq-config-form.vue';
export {default as MqttConfigForm} from './mqtt-config-form.vue';
export {default as RabbitMqConfigForm} from './rabbit-mq-config-form.vue';
export {default as RedisStreamConfigForm} from './redis-stream-config-form.vue';
export {default as RocketMqConfigForm} from './rocket-mq-config-form.vue';

View File

@@ -15,11 +15,11 @@ import { $t } from '#/locales';
import {
HttpConfigForm,
KafkaMQConfigForm,
KafkaMqConfigForm,
MqttConfigForm,
RabbitMQConfigForm,
RabbitMqConfigForm,
RedisStreamConfigForm,
RocketMQConfigForm,
RocketMqConfigForm,
} from './config';
import { useSinkFormSchema } from './data';
@@ -120,6 +120,7 @@ watch(
<Form class="mx-4" />
<div v-if="formData" class="mx-4 mt-4">
<div class="mb-2 text-sm font-medium">配置信息</div>
<!-- TODO @haohao下面的 form看看有没办法搞成 form schema 方便后续 ele 的迁移 -->
<HttpConfigForm
v-if="IotDataSinkTypeEnum.HTTP === formData.type"
v-model="formData.config"
@@ -128,15 +129,15 @@ watch(
v-if="IotDataSinkTypeEnum.MQTT === formData.type"
v-model="formData.config"
/>
<RocketMQConfigForm
<RocketMqConfigForm
v-if="IotDataSinkTypeEnum.ROCKETMQ === formData.type"
v-model="formData.config"
/>
<KafkaMQConfigForm
<KafkaMqConfigForm
v-if="IotDataSinkTypeEnum.KAFKA === formData.type"
v-model="formData.config"
/>
<RabbitMQConfigForm
<RabbitMqConfigForm
v-if="IotDataSinkTypeEnum.RABBITMQ === formData.type"
v-model="formData.config"
/>

View File

@@ -10,9 +10,11 @@ import { deleteDataSink, getDataSinkPage } from '#/api/iot/rule/data/sink';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import DataSinkForm from './DataSinkForm.vue';
import DataSinkForm from './data-sink-form.vue';
/** IoT 数据流转目的 列表 */
// TODO @haohao需要根据代码规范在优化下这个模块。和别的模块的风格保持一致。
/** IoT 数据流转目的列表 */
defineOptions({ name: 'IotDataSink' });
const [FormModal, formModalApi] = useVbenModal({
@@ -43,9 +45,7 @@ async function handleDelete(row: any) {
});
try {
await deleteDataSink(row.id);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
});
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
handleRefresh();
} finally {
hideLoading();

View File

@@ -1,44 +0,0 @@
<script lang="ts" setup>
import { Page } from '@vben/common-ui';
import { Button } from 'ant-design-vue';
defineOptions({ name: 'IotRuleDataBridge' });
</script>
<template>
<Page description="物聯網規則引擎 - 數據橋接" title="數據橋接">
<div class="p-4">
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/tree/master/src/views/iot/rule"
>
該功能支持 Vue3 + element-plus 版本
</Button>
<br />
<Button
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/iot/rule/data/rule/index.vue"
>
可參考源代碼進行遷移
</Button>
<div class="mt-4">
<h3>功能說明</h3>
<p>規則引擎包括</p>
<ul>
<li>數據規則配置</li>
<li>數據轉發配置</li>
<li>場景聯動配置</li>
</ul>
<h3 class="mt-4">待實現</h3>
<ul>
<li> API 接口定義</li>
<li> 頁面實現</li>
</ul>
</div>
</div>
</Page>
</template>

View File

@@ -14,12 +14,12 @@ import {
IotRuleSceneTriggerConditionTypeEnum,
} from '#/views/iot/utils/constants';
import ValueInput from '../inputs/ValueInput.vue';
import DeviceSelector from '../selectors/DeviceSelector.vue';
import OperatorSelector from '../selectors/OperatorSelector.vue';
import ProductSelector from '../selectors/ProductSelector.vue';
import PropertySelector from '../selectors/PropertySelector.vue';
import CurrentTimeConditionConfig from './CurrentTimeConditionConfig.vue';
import ValueInput from '../inputs/value-input.vue';
import DeviceSelector from '../selectors/device-selector.vue';
import OperatorSelector from '../selectors/operator-selector.vue';
import ProductSelector from '../selectors/product-selector.vue';
import PropertySelector from '../selectors/property-selector.vue';
import CurrentTimeConditionConfig from './current-time-condition-config.vue';
/** 单个条件配置组件 */
defineOptions({ name: 'ConditionConfig' });

View File

@@ -20,9 +20,9 @@ import {
IoTThingModelAccessModeEnum,
} from '#/views/iot/utils/constants';
import JsonParamsInput from '../inputs/JsonParamsInput.vue';
import DeviceSelector from '../selectors/DeviceSelector.vue';
import ProductSelector from '../selectors/ProductSelector.vue';
import JsonParamsInput from '../inputs/json-params-input.vue';
import DeviceSelector from '../selectors/device-selector.vue';
import ProductSelector from '../selectors/product-selector.vue';
/** 设备控制配置组件 */
defineOptions({ name: 'DeviceControlConfig' });

View File

@@ -9,8 +9,8 @@ import { IconifyIcon } from '@vben/icons';
import { useVModel } from '@vueuse/core';
import { Button, Tag } from 'ant-design-vue';
import MainConditionInnerConfig from './MainConditionInnerConfig.vue';
import SubConditionGroupConfig from './SubConditionGroupConfig.vue';
import MainConditionInnerConfig from './main-condition-inner-config.vue';
import SubConditionGroupConfig from './sub-condition-group-config.vue';
/** 设备触发配置组件 */
defineOptions({ name: 'DeviceTriggerConfig' });

View File

@@ -14,12 +14,12 @@ import {
triggerTypeOptions,
} from '#/views/iot/utils/constants';
import JsonParamsInput from '../inputs/JsonParamsInput.vue';
import ValueInput from '../inputs/ValueInput.vue';
import DeviceSelector from '../selectors/DeviceSelector.vue';
import OperatorSelector from '../selectors/OperatorSelector.vue';
import ProductSelector from '../selectors/ProductSelector.vue';
import PropertySelector from '../selectors/PropertySelector.vue';
import JsonParamsInput from '../inputs/json-params-input.vue';
import ValueInput from '../inputs/value-input.vue';
import DeviceSelector from '../selectors/device-selector.vue';
import OperatorSelector from '../selectors/operator-selector.vue';
import ProductSelector from '../selectors/product-selector.vue';
import PropertySelector from '../selectors/property-selector.vue';
/** 主条件内部配置组件 */
defineOptions({ name: 'MainConditionInnerConfig' });

View File

@@ -13,7 +13,7 @@ import {
IotRuleSceneTriggerConditionTypeEnum,
} from '#/views/iot/utils/constants';
import ConditionConfig from './ConditionConfig.vue';
import ConditionConfig from './condition-config.vue';
/** 子条件组配置组件 */
defineOptions({ name: 'SubConditionGroupConfig' });

View File

@@ -16,9 +16,9 @@ import {
isDeviceTrigger,
} from '#/views/iot/utils/constants';
import ActionSection from './sections/ActionSection.vue';
import BasicInfoSection from './sections/BasicInfoSection.vue';
import TriggerSection from './sections/TriggerSection.vue';
import ActionSection from './sections/action-section.vue';
import BasicInfoSection from './sections/basic-info-section.vue';
import TriggerSection from './sections/trigger-section.vue';
/** IoT 场景联动规则表单 - 主表单组件 */
defineOptions({ name: 'RuleSceneForm' });

View File

@@ -13,8 +13,8 @@ import {
IotRuleSceneActionTypeEnum,
} from '#/views/iot/utils/constants';
import AlertConfig from '../configs/AlertConfig.vue';
import DeviceControlConfig from '../configs/DeviceControlConfig.vue';
import AlertConfig from '../configs/alert-config.vue';
import DeviceControlConfig from '../configs/device-control-config.vue';
/** 执行器配置组件 */
defineOptions({ name: 'ActionSection' });

View File

@@ -8,6 +8,7 @@ import { IconifyIcon } from '@vben/icons';
import { useVModel } from '@vueuse/core';
import { Card, Col, Form, Input, Radio, Row } from 'ant-design-vue';
import { DictTag } from "#/components/dict-tag";
/** 基础信息配置组件 */
defineOptions({ name: 'BasicInfoSection' });

View File

@@ -15,7 +15,7 @@ import {
isDeviceTrigger,
} from '#/views/iot/utils/constants';
import DeviceTriggerConfig from '../configs/DeviceTriggerConfig.vue';
import DeviceTriggerConfig from '../configs/device-trigger-config.vue';
/** 触发器配置组件 */
defineOptions({ name: 'TriggerSection' });

View File

@@ -7,6 +7,7 @@ import { DICT_TYPE } from '@vben/constants';
import { Select } from 'ant-design-vue';
import { getSimpleProductList } from '#/api/iot/product/product';
import { DictTag } from "#/components/dict-tag";
/** 产品选择器组件 */
defineOptions({ name: 'ProductSelector' });

View File

@@ -14,8 +14,8 @@ import { deleteThingModel, getThingModelPage } from '#/api/iot/thingmodel';
import { getDataTypeOptionsLabel, IOT_PROVIDE_KEY } from '../utils/constants';
import { useGridColumns, useGridFormSchema } from './data';
import { DataDefinition } from './modules/components';
import ThingModelForm from './modules/ThingModelForm.vue';
import ThingModelTSL from './modules/ThingModelTSL.vue';
import ThingModelForm from './modules/thing-model-form.vue';
import ThingModelTsl from './modules/thing-model-tsl.vue';
defineOptions({ name: 'IoTThingModel' });
@@ -23,16 +23,65 @@ const props = defineProps<{
productId: number;
}>();
// 产品信息
const product = ref<IotProductApi.Product>({} as IotProductApi.Product);
const product = ref<IotProductApi.Product>({} as IotProductApi.Product); // 产品信息
// 提供产品信息给子组件
provide(IOT_PROVIDE_KEY.PRODUCT, product);
provide(IOT_PROVIDE_KEY.PRODUCT, product); // 提供产品信息给子组件
// 组件引用
// 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();
}
// 获取产品信息
async function getProductData() {
try {
product.value = await getProduct(props.productId);
} catch (error) {
console.error('获取产品信息失败:', error);
}
}
// TODO @haohao字段的顺序调整成别的模块一直
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: useGridColumns(),
@@ -64,51 +113,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
},
});
// 新增功能
function handleCreate() {
thingModelFormRef.value?.open('create');
}
// 编辑功能
function handleEdit(row: any) {
thingModelFormRef.value?.open('update', row.id);
}
// 删除功能
async function handleDelete(row: any) {
try {
await deleteThingModel(row.id);
message.success('删除成功');
gridApi.reload();
} catch (error) {
console.error('删除失败:', error);
}
}
// 打开 TSL
function handleOpenTSL() {
thingModelTSLRef.value?.open();
}
// 获取数据类型标签
function getDataTypeLabel(row: any) {
return getDataTypeOptionsLabel(row.property?.dataType) || '-';
}
// 刷新表格
function handleRefresh() {
gridApi.reload();
}
// 获取产品信息
async function getProductData() {
try {
product.value = await getProduct(props.productId);
} catch (error) {
console.error('获取产品信息失败:', error);
}
}
// 初始化
onMounted(async () => {
await getProductData();
@@ -116,11 +120,7 @@ onMounted(async () => {
</script>
<template>
<Page
auto-content-height
description="管理产品的物模型定义,包括属性、服务和事件"
title="物模型管理"
>
<Page auto-content-height>
<Grid>
<template #toolbar-tools>
<TableAction
@@ -134,23 +134,21 @@ onMounted(async () => {
{
label: 'TSL',
type: 'default',
color: 'success',
color: 'success', // TODO @haohao貌似 color 可以去掉应该是不生效的哈。ps另外也给搞个 icon
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
@@ -178,8 +176,7 @@ onMounted(async () => {
<!-- 物模型表单 -->
<ThingModelForm ref="thingModelFormRef" @success="handleRefresh" />
<!-- TSL 弹窗 -->
<ThingModelTSL ref="thingModelTSLRef" />
<ThingModelTsl ref="thingModelTSLRef" />
</Page>
</template>

View File

@@ -1,3 +1,4 @@
<!-- TODO @haohao如果是模块内用的就用 modules 等后面点在看优先级 -->
<script lang="ts" setup>
import type { ThingModelData } from '#/api/iot/thingmodel';
@@ -17,7 +18,6 @@ defineOptions({ name: 'DataDefinition' });
const props = defineProps<{ data: ThingModelData }>();
//
const formattedDataSpecsList = computed(() => {
if (
!props.data.property?.dataSpecsList ||
@@ -28,9 +28,8 @@ const formattedDataSpecsList = computed(() => {
return props.data.property.dataSpecsList
.map((item) => `${item.value}-${item.name}`)
.join('、');
});
}); //
//
const shortText = computed(() => {
if (
!props.data.property?.dataSpecsList ||
@@ -43,7 +42,7 @@ const shortText = computed(() => {
return count > 1
? `${first.value}-${first.name}${count}`
: `${first.value}-${first.name}`;
});
}); //
</script>
<template>
@@ -100,9 +99,8 @@ const shortText = computed(() => {
</template>
<!-- 服务 -->
<div v-if="Number(data.type) === IoTThingModelTypeEnum.SERVICE">
调用方式{{
getThingModelServiceCallTypeLabel(data.service?.callType as any)
}}
调用方式
{{ getThingModelServiceCallTypeLabel(data.service?.callType as any) }}
</div>
<!-- 事件 -->
<div v-if="Number(data.type) === IoTThingModelTypeEnum.EVENT">
@@ -111,6 +109,7 @@ const shortText = computed(() => {
</template>
<style lang="scss" scoped>
/** TODO @haohaotindwind */
.data-specs-text {
cursor: help;
border-bottom: 1px dashed #d9d9d9;

View File

@@ -1 +1 @@
export { default as DataDefinition } from './DataDefinition.vue';
export { default as DataDefinition } from './data-definition.vue';

View File

@@ -1,4 +1,4 @@
export { default as ThingModelArrayDataSpecs } from './ThingModelArrayDataSpecs.vue';
export { default as ThingModelEnumDataSpecs } from './ThingModelEnumDataSpecs.vue';
export { default as ThingModelNumberDataSpecs } from './ThingModelNumberDataSpecs.vue';
export { default as ThingModelStructDataSpecs } from './ThingModelStructDataSpecs.vue';
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

@@ -10,7 +10,7 @@ import {
IoTDataSpecsDataTypeEnum,
} from '#/views/iot/utils/constants';
import ThingModelStructDataSpecs from './ThingModelStructDataSpecs.vue';
import ThingModelStructDataSpecs from './thing-model-struct-data-specs.vue';
/** 数组型的 dataSpecs 配置组件 */
defineOptions({ name: 'ThingModelArrayDataSpecs' });

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