This commit is contained in:
hw
2025-11-21 18:20:07 +08:00
31 changed files with 182 additions and 211 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,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

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

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

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

@@ -26,17 +26,14 @@ 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';
@@ -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';
@@ -51,8 +52,9 @@ function openForm(type: string, id?: number) {
/** 发布产品 */
async function confirmPublish(id: number) {
// TODO @haohao最好类似async 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 @haohao最好类似async 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';
@@ -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

@@ -21,6 +21,7 @@ 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

@@ -12,6 +12,8 @@ import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
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

@@ -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"

View File

@@ -12,7 +12,9 @@ import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
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

@@ -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 './data-definition.vue';
export { default as DataDefinition } from './data-definition.vue';

View File

@@ -23,7 +23,7 @@ const props = defineProps<{ isStructDataSpecs?: boolean; modelValue: any }>();
const emits = defineEmits(['update:modelValue']);
const thingModelEvent = useVModel(props, 'modelValue', emits) as Ref<any>;
// 默认选中INFO 信息
/** 默认选中INFO 信息 */
watch(
() => thingModelEvent.value.type,
(val: string | undefined) =>

View File

@@ -2,6 +2,7 @@
<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';
@@ -61,6 +62,7 @@ const formData = ref<any>({
const formRef = ref(); // 表单 Ref
/** 打开弹窗 */
// TODO @haohaoModal 的写法。
async function open(type: string, id?: number) {
dialogVisible.value = true;
// 设置标题create -> 新增update -> 编辑
@@ -244,6 +246,7 @@ function resetForm() {
:confirm-loading="formLoading"
@ok="submitForm"
>
<!-- TODO @haohao这个可以改造成 data.ts schema 形式么可能是有一定成本后续迁移 ele 版本会容易很多 -->
<Form
ref="formRef"
:model="formData"

View File

@@ -10,7 +10,7 @@ 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'});
defineOptions({ name: 'ThingModelTsl' });
const dialogVisible = ref(false); // 弹窗的是否展示
const dialogTitle = ref('物模型 TSL'); // 弹窗的标题

View File

@@ -0,0 +1 @@
// TODO @haohao枚举可以放到这里

View File

@@ -2,6 +2,7 @@ export * from './biz-ai-enum';
export * from './biz-bpm-enum';
export * from './biz-erp-enum';
export * from './biz-infra-enum';
export * from './biz-iot-enum';
export * from './biz-mall-enum';
export * from './biz-mp-enum';
export * from './biz-pay-enum';