mirror of
https://gitee.com/yudaocode/yudao-ui-admin-vben.git
synced 2025-12-30 10:32:25 +00:00
Merge remote-tracking branch 'yudao/dev' into dev
This commit is contained in:
@@ -18,8 +18,7 @@ const routes: RouteRecordRaw[] = [
|
||||
title: '产品详情',
|
||||
activePath: '/iot/device/product',
|
||||
},
|
||||
component: () =>
|
||||
import('#/views/iot/product/product/modules/detail/index.vue'),
|
||||
component: () => import('#/views/iot/product/product/detail/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'device/detail/:id',
|
||||
@@ -28,8 +27,7 @@ const routes: RouteRecordRaw[] = [
|
||||
title: '设备详情',
|
||||
activePath: '/iot/device/device',
|
||||
},
|
||||
component: () =>
|
||||
import('#/views/iot/device/device/modules/detail/index.vue'),
|
||||
component: () => import('#/views/iot/device/device/detail/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'ota/firmware/detail/:id',
|
||||
|
||||
@@ -35,8 +35,6 @@ const device = ref<IotDeviceApi.Device>({} as IotDeviceApi.Device);
|
||||
const activeTab = ref('info');
|
||||
const thingModelList = ref<ThingModelData[]>([]);
|
||||
|
||||
// TODO @haohao:类似 device/detail/index.vue 挪出去哈。
|
||||
|
||||
/** 获取设备详情 */
|
||||
async function getDeviceData(deviceId: number) {
|
||||
loading.value = true;
|
||||
@@ -21,6 +21,7 @@ const emit = defineEmits<{
|
||||
|
||||
const loading = ref(false); // 加载中
|
||||
const pushLoading = ref(false); // 推送加载中
|
||||
const saveLoading = ref(false); // 保存加载中
|
||||
const config = ref<any>({}); // 只存储 config 字段
|
||||
const configString = ref(''); // 用于编辑器的字符串格式
|
||||
|
||||
@@ -50,11 +51,6 @@ const formattedConfig = computed(() => {
|
||||
}
|
||||
});
|
||||
|
||||
/** 判断配置是否有数据 */
|
||||
const hasConfigData = computed(() => {
|
||||
return config.value && Object.keys(config.value).length > 0;
|
||||
});
|
||||
|
||||
/** 启用编辑模式的函数 */
|
||||
function handleEdit() {
|
||||
isEditing.value = true;
|
||||
@@ -84,9 +80,13 @@ async function saveConfig() {
|
||||
message.error({ content: 'JSON格式错误,请修正后再提交!' });
|
||||
return;
|
||||
}
|
||||
// TODO @haohao:这里要不要做个类似下面的 pushLoading 避免重复提交;
|
||||
await updateDeviceConfig();
|
||||
isEditing.value = false;
|
||||
saveLoading.value = true;
|
||||
try {
|
||||
await updateDeviceConfig();
|
||||
isEditing.value = false;
|
||||
} finally {
|
||||
saveLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 配置推送处理函数 */
|
||||
@@ -126,36 +126,14 @@ async function updateDeviceConfig() {
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- 只在没有配置数据时显示提示 -->
|
||||
<!-- 使用说明提示 -->
|
||||
<Alert
|
||||
v-if="!hasConfigData"
|
||||
message="支持远程更新设备的配置文件(JSON 格式),可以在下方编辑配置模板,对设备的系统参数、网络参数等进行远程配置。配置完成后,需点击「下发」按钮,设备即可进行远程配置。"
|
||||
message="支持远程更新设备的配置文件(JSON 格式),可以在下方编辑配置模板,对设备的系统参数、网络参数等进行远程配置。配置完成后,需点击「配置推送」按钮,设备即可进行远程配置。"
|
||||
type="info"
|
||||
show-icon
|
||||
class="my-4"
|
||||
description="如需编辑文件,请点击下方编辑按钮"
|
||||
/>
|
||||
<!-- TODO @haohao:应该按钮,是在下方,可以参考 element-plus 的版本 -->
|
||||
<div class="mt-5 text-center">
|
||||
<Button v-if="isEditing" @click="handleCancelEdit">取消</Button>
|
||||
<Button
|
||||
v-if="isEditing"
|
||||
type="primary"
|
||||
@click="saveConfig"
|
||||
:loading="loading"
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
<Button v-else @click="handleEdit">编辑</Button>
|
||||
<Button
|
||||
v-if="!isEditing"
|
||||
type="primary"
|
||||
@click="handleConfigPush"
|
||||
:loading="pushLoading"
|
||||
>
|
||||
配置推送
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- 代码视图 - 只读展示 -->
|
||||
<div v-if="!isEditing" class="json-viewer-container">
|
||||
@@ -170,6 +148,28 @@ async function updateDeviceConfig() {
|
||||
placeholder="请输入 JSON 格式的配置信息"
|
||||
class="json-editor"
|
||||
/>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="mt-5 text-center">
|
||||
<Button v-if="isEditing" @click="handleCancelEdit">取消</Button>
|
||||
<Button
|
||||
v-if="isEditing"
|
||||
type="primary"
|
||||
@click="saveConfig"
|
||||
:loading="saveLoading"
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
<Button v-else @click="handleEdit">编辑</Button>
|
||||
<Button
|
||||
v-if="!isEditing"
|
||||
type="primary"
|
||||
@click="handleConfigPush"
|
||||
:loading="pushLoading"
|
||||
>
|
||||
配置推送
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Button, Card, Descriptions, message } from 'ant-design-vue';
|
||||
|
||||
import DeviceForm from '../../form.vue';
|
||||
import DeviceForm from '../../modules/form.vue';
|
||||
|
||||
interface Props {
|
||||
product: IotProductApi.Product;
|
||||
@@ -74,7 +74,7 @@ function openEditForm(row: IotDeviceApi.Device) {
|
||||
</div>
|
||||
|
||||
<Card class="mt-4">
|
||||
<Descriptions :column="1">
|
||||
<Descriptions :column="2">
|
||||
<Descriptions.Item label="产品">
|
||||
<a
|
||||
class="cursor-pointer text-blue-600"
|
||||
@@ -6,7 +6,7 @@ import { computed, ref } from 'vue';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { formatDate } from '@vben/utils';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import {
|
||||
Button,
|
||||
@@ -92,6 +92,12 @@ function handleAuthInfoDialogClose() {
|
||||
:value="props.product.deviceType"
|
||||
/>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="定位类型">
|
||||
<DictTag
|
||||
:type="DICT_TYPE.IOT_LOCATION_TYPE"
|
||||
:value="props.product.locationType"
|
||||
/>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="DeviceName">
|
||||
{{ props.device.deviceName }}
|
||||
</Descriptions.Item>
|
||||
@@ -105,16 +111,16 @@ function handleAuthInfoDialogClose() {
|
||||
/>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="创建时间">
|
||||
{{ formatDate(props.device.createTime) }}
|
||||
{{ formatDateTime(props.device.createTime) }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="激活时间">
|
||||
{{ formatDate(props.device.activeTime) }}
|
||||
{{ formatDateTime(props.device.activeTime) }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="最后上线时间">
|
||||
{{ formatDate(props.device.onlineTime) }}
|
||||
{{ formatDateTime(props.device.onlineTime) }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="最后离线时间">
|
||||
{{ formatDate(props.device.offlineTime) }}
|
||||
{{ formatDateTime(props.device.offlineTime) }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="MQTT 连接参数">
|
||||
<Button
|
||||
@@ -138,6 +144,9 @@ function handleAuthInfoDialogClose() {
|
||||
<IconifyIcon icon="ep:location" class="mr-2 text-primary" />
|
||||
<span>设备位置</span>
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
最后上线:{{ formatDateTime(props.device.onlineTime) || '--' }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="h-[500px] w-full">
|
||||
@@ -0,0 +1,252 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import {
|
||||
computed,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
reactive,
|
||||
ref,
|
||||
watch,
|
||||
} from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Select,
|
||||
Space,
|
||||
Switch,
|
||||
Tag,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getDeviceMessagePage } from '#/api/iot/device/device';
|
||||
import { IotDeviceMessageMethodEnum } from '#/views/iot/utils/constants';
|
||||
|
||||
const props = defineProps<{
|
||||
deviceId: number;
|
||||
}>();
|
||||
|
||||
/** 查询参数 */
|
||||
const queryParams = reactive({
|
||||
method: undefined,
|
||||
upstream: undefined,
|
||||
});
|
||||
|
||||
/** 自动刷新开关 */
|
||||
const autoRefresh = ref(false);
|
||||
/** 自动刷新定时器 */
|
||||
let autoRefreshTimer: any = null;
|
||||
|
||||
/** 消息方法选项 */
|
||||
const methodOptions = computed(() => {
|
||||
return Object.values(IotDeviceMessageMethodEnum).map((item) => ({
|
||||
label: item.name,
|
||||
value: item.method,
|
||||
}));
|
||||
});
|
||||
|
||||
/** Grid 列定义 */
|
||||
function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'ts',
|
||||
title: '时间',
|
||||
width: 160,
|
||||
slots: { default: 'ts' },
|
||||
},
|
||||
{
|
||||
field: 'upstream',
|
||||
title: '上行/下行',
|
||||
width: 100,
|
||||
slots: { default: 'upstream' },
|
||||
},
|
||||
{
|
||||
field: 'reply',
|
||||
title: '是否回复',
|
||||
width: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'requestId',
|
||||
title: '请求编号',
|
||||
width: 280,
|
||||
showOverflow: 'tooltip',
|
||||
},
|
||||
{
|
||||
field: 'method',
|
||||
title: '请求方法',
|
||||
width: 120,
|
||||
slots: { default: 'method' },
|
||||
},
|
||||
{
|
||||
field: 'params',
|
||||
title: '请求/响应数据',
|
||||
minWidth: 200,
|
||||
showOverflow: 'tooltip',
|
||||
slots: { default: 'params' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 创建 Grid 实例 */
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }) => {
|
||||
if (!props.deviceId) {
|
||||
return { list: [], total: 0 };
|
||||
}
|
||||
return await getDeviceMessagePage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
deviceId: props.deviceId,
|
||||
method: queryParams.method,
|
||||
upstream: queryParams.upstream,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: false,
|
||||
search: false,
|
||||
},
|
||||
pagerConfig: {
|
||||
enabled: true,
|
||||
},
|
||||
} as VxeTableGridOptions,
|
||||
});
|
||||
|
||||
/** 搜索操作 */
|
||||
function handleQuery() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 监听自动刷新 */
|
||||
watch(autoRefresh, (newValue) => {
|
||||
if (newValue) {
|
||||
autoRefreshTimer = setInterval(() => {
|
||||
gridApi.query();
|
||||
}, 5000);
|
||||
} else {
|
||||
clearInterval(autoRefreshTimer);
|
||||
autoRefreshTimer = null;
|
||||
}
|
||||
});
|
||||
|
||||
/** 监听设备标识变化 */
|
||||
watch(
|
||||
() => props.deviceId,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
handleQuery();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
/** 组件卸载时清除定时器 */
|
||||
onBeforeUnmount(() => {
|
||||
if (autoRefreshTimer) {
|
||||
clearInterval(autoRefreshTimer);
|
||||
autoRefreshTimer = null;
|
||||
}
|
||||
});
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
if (props.deviceId) {
|
||||
handleQuery();
|
||||
}
|
||||
});
|
||||
|
||||
/** 刷新消息列表 */
|
||||
function refresh(delay = 0) {
|
||||
if (delay > 0) {
|
||||
setTimeout(() => {
|
||||
gridApi.query();
|
||||
}, delay);
|
||||
} else {
|
||||
gridApi.query();
|
||||
}
|
||||
}
|
||||
|
||||
/** 暴露方法给父组件 */
|
||||
defineExpose({
|
||||
refresh,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<!-- 搜索区域 -->
|
||||
<div class="mb-4 flex flex-wrap items-center gap-3">
|
||||
<Select
|
||||
v-model:value="queryParams.method"
|
||||
placeholder="所有方法"
|
||||
style="width: 160px"
|
||||
allow-clear
|
||||
>
|
||||
<Select.Option
|
||||
v-for="item in methodOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
<Select
|
||||
v-model:value="queryParams.upstream"
|
||||
placeholder="上行/下行"
|
||||
style="width: 160px"
|
||||
allow-clear
|
||||
>
|
||||
<Select.Option label="上行" value="true">上行</Select.Option>
|
||||
<Select.Option label="下行" value="false">下行</Select.Option>
|
||||
</Select>
|
||||
<Space>
|
||||
<Button type="primary" @click="handleQuery">
|
||||
<IconifyIcon icon="ep:search" class="mr-5px" /> 搜索
|
||||
</Button>
|
||||
<Switch
|
||||
v-model:checked="autoRefresh"
|
||||
checked-children="定时刷新"
|
||||
un-checked-children="定时刷新"
|
||||
/>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<!-- 消息列表 -->
|
||||
<Grid>
|
||||
<template #ts="{ row }">
|
||||
{{ formatDateTime(row.ts) }}
|
||||
</template>
|
||||
<template #upstream="{ row }">
|
||||
<Tag :color="row.upstream ? 'blue' : 'green'">
|
||||
{{ row.upstream ? '上行' : '下行' }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template #method="{ row }">
|
||||
{{ methodOptions.find((item) => item.value === row.method)?.label }}
|
||||
</template>
|
||||
<template #params="{ row }">
|
||||
<span v-if="row.reply">
|
||||
{{
|
||||
`{"code":${row.code},"msg":"${row.msg}","data":${row.data}\}`
|
||||
}}
|
||||
</span>
|
||||
<span v-else>{{ row.params }}</span>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,612 @@
|
||||
<!-- 模拟设备 -->
|
||||
<script lang="ts" setup>
|
||||
import type { TableColumnType } from 'ant-design-vue';
|
||||
|
||||
import type { IotDeviceApi } from '#/api/iot/device/device';
|
||||
import type { IotProductApi } from '#/api/iot/product/product';
|
||||
import type { ThingModelData } from '#/api/iot/thingmodel';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { ContentWrap } from '@vben/common-ui';
|
||||
import { DeviceStateEnum } from '@vben/constants';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Input,
|
||||
message,
|
||||
Row,
|
||||
Table,
|
||||
Tabs,
|
||||
Textarea,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { sendDeviceMessage } from '#/api/iot/device/device';
|
||||
import {
|
||||
IotDeviceMessageMethodEnum,
|
||||
IoTThingModelTypeEnum,
|
||||
} from '#/views/iot/utils/constants';
|
||||
|
||||
import DataDefinition from '../../../../thingmodel/modules/components/data-definition.vue';
|
||||
import DeviceDetailsMessage from './message.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
device: IotDeviceApi.Device;
|
||||
product: IotProductApi.Product;
|
||||
thingModelList: ThingModelData[];
|
||||
}>();
|
||||
|
||||
// 消息弹窗
|
||||
const activeTab = ref('upstream'); // 上行upstream、下行downstream
|
||||
const upstreamTab = ref(IotDeviceMessageMethodEnum.PROPERTY_POST.method); // 上行子标签
|
||||
const downstreamTab = ref(IotDeviceMessageMethodEnum.PROPERTY_SET.method); // 下行子标签
|
||||
const deviceMessageRef = ref(); // 设备消息组件引用
|
||||
const deviceMessageRefreshDelay = 2000; // 延迟 N 秒,保证模拟上行的消息被处理
|
||||
|
||||
// 折叠状态
|
||||
const debugCollapsed = ref(false); // 指令调试区域折叠状态
|
||||
const messageCollapsed = ref(false); // 设备消息区域折叠状态
|
||||
|
||||
// 表单数据:存储用户输入的模拟值
|
||||
const formData = ref<Record<string, string>>({});
|
||||
|
||||
// 根据类型过滤物模型数据
|
||||
const getFilteredThingModelList = (type: number) => {
|
||||
return props.thingModelList.filter(
|
||||
(item) => String(item.type) === String(type),
|
||||
);
|
||||
};
|
||||
|
||||
// 计算属性:属性列表
|
||||
const propertyList = computed(() =>
|
||||
getFilteredThingModelList(IoTThingModelTypeEnum.PROPERTY),
|
||||
);
|
||||
|
||||
// 计算属性:事件列表
|
||||
const eventList = computed(() =>
|
||||
getFilteredThingModelList(IoTThingModelTypeEnum.EVENT),
|
||||
);
|
||||
|
||||
// 计算属性:服务列表
|
||||
const serviceList = computed(() =>
|
||||
getFilteredThingModelList(IoTThingModelTypeEnum.SERVICE),
|
||||
);
|
||||
|
||||
// 属性表格列定义
|
||||
const propertyColumns: TableColumnType[] = [
|
||||
{
|
||||
title: '功能名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 100,
|
||||
fixed: 'left' as any,
|
||||
},
|
||||
{
|
||||
title: '标识符',
|
||||
dataIndex: 'identifier',
|
||||
key: 'identifier',
|
||||
width: 120,
|
||||
fixed: 'left' as any,
|
||||
},
|
||||
{
|
||||
title: '数据类型',
|
||||
key: 'dataType',
|
||||
width: 90,
|
||||
},
|
||||
{
|
||||
title: '数据定义',
|
||||
key: 'dataDefinition',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '值',
|
||||
key: 'value',
|
||||
width: 180,
|
||||
fixed: 'right' as any,
|
||||
},
|
||||
];
|
||||
|
||||
// 事件表格列定义
|
||||
const eventColumns = [
|
||||
{
|
||||
title: '功能名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 100,
|
||||
fixed: 'left' as any,
|
||||
},
|
||||
{
|
||||
title: '标识符',
|
||||
dataIndex: 'identifier',
|
||||
key: 'identifier',
|
||||
width: 120,
|
||||
fixed: 'left' as any,
|
||||
},
|
||||
{
|
||||
title: '数据类型',
|
||||
key: 'dataType',
|
||||
width: 90,
|
||||
},
|
||||
{
|
||||
title: '数据定义',
|
||||
key: 'dataDefinition',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '值',
|
||||
key: 'value',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 100,
|
||||
fixed: 'right' as any,
|
||||
},
|
||||
];
|
||||
|
||||
// 服务表格列定义
|
||||
const serviceColumns = [
|
||||
{
|
||||
title: '服务名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 100,
|
||||
fixed: 'left' as any,
|
||||
},
|
||||
{
|
||||
title: '标识符',
|
||||
dataIndex: 'identifier',
|
||||
key: 'identifier',
|
||||
width: 120,
|
||||
fixed: 'left' as any,
|
||||
},
|
||||
{
|
||||
title: '输入参数',
|
||||
key: 'dataDefinition',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '参数值',
|
||||
key: 'value',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 100,
|
||||
fixed: 'right' as any,
|
||||
},
|
||||
];
|
||||
|
||||
// 获取表单值
|
||||
function getFormValue(identifier: string) {
|
||||
return formData.value[identifier] || '';
|
||||
}
|
||||
|
||||
// 设置表单值
|
||||
function setFormValue(identifier: string, value: string) {
|
||||
formData.value[identifier] = value;
|
||||
}
|
||||
|
||||
// 属性上报
|
||||
async function handlePropertyPost() {
|
||||
try {
|
||||
const params: Record<string, any> = {};
|
||||
propertyList.value.forEach((item) => {
|
||||
const value = formData.value[item.identifier!];
|
||||
if (value) {
|
||||
params[item.identifier!] = value;
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(params).length === 0) {
|
||||
message.warning({ content: '请至少输入一个属性值' });
|
||||
return;
|
||||
}
|
||||
|
||||
await sendDeviceMessage({
|
||||
deviceId: props.device.id!,
|
||||
method: IotDeviceMessageMethodEnum.PROPERTY_POST.method,
|
||||
params,
|
||||
});
|
||||
|
||||
message.success({ content: '属性上报成功' });
|
||||
// 延迟刷新设备消息列表
|
||||
deviceMessageRef.value?.refresh(deviceMessageRefreshDelay);
|
||||
} catch (error) {
|
||||
message.error({ content: '属性上报失败' });
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
// 事件上报
|
||||
async function handleEventPost(row: ThingModelData) {
|
||||
try {
|
||||
const valueStr = formData.value[row.identifier!];
|
||||
let params: any = {};
|
||||
|
||||
if (valueStr) {
|
||||
try {
|
||||
params = JSON.parse(valueStr);
|
||||
} catch {
|
||||
message.error({ content: '事件参数格式错误,请输入有效的JSON格式' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await sendDeviceMessage({
|
||||
deviceId: props.device.id!,
|
||||
method: IotDeviceMessageMethodEnum.EVENT_POST.method,
|
||||
params: {
|
||||
identifier: row.identifier,
|
||||
params,
|
||||
},
|
||||
});
|
||||
|
||||
message.success({ content: '事件上报成功' });
|
||||
// 延迟刷新设备消息列表
|
||||
deviceMessageRef.value?.refresh(deviceMessageRefreshDelay);
|
||||
} catch (error) {
|
||||
message.error({ content: '事件上报失败' });
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
// 状态变更
|
||||
async function handleDeviceState(state: number) {
|
||||
try {
|
||||
await sendDeviceMessage({
|
||||
deviceId: props.device.id!,
|
||||
method: IotDeviceMessageMethodEnum.STATE_UPDATE.method,
|
||||
params: { state },
|
||||
});
|
||||
|
||||
message.success({ content: '状态变更成功' });
|
||||
// 延迟刷新设备消息列表
|
||||
deviceMessageRef.value?.refresh(deviceMessageRefreshDelay);
|
||||
} catch (error) {
|
||||
message.error({ content: '状态变更失败' });
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
// 属性设置
|
||||
async function handlePropertySet() {
|
||||
try {
|
||||
const params: Record<string, any> = {};
|
||||
propertyList.value.forEach((item) => {
|
||||
const value = formData.value[item.identifier!];
|
||||
if (value) {
|
||||
params[item.identifier!] = value;
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(params).length === 0) {
|
||||
message.warning({ content: '请至少输入一个属性值' });
|
||||
return;
|
||||
}
|
||||
|
||||
await sendDeviceMessage({
|
||||
deviceId: props.device.id!,
|
||||
method: IotDeviceMessageMethodEnum.PROPERTY_SET.method,
|
||||
params,
|
||||
});
|
||||
|
||||
message.success({ content: '属性设置成功' });
|
||||
// 延迟刷新设备消息列表
|
||||
deviceMessageRef.value?.refresh(deviceMessageRefreshDelay);
|
||||
} catch (error) {
|
||||
message.error({ content: '属性设置失败' });
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
// 服务调用
|
||||
async function handleServiceInvoke(row: ThingModelData) {
|
||||
try {
|
||||
const valueStr = formData.value[row.identifier!];
|
||||
let params: any = {};
|
||||
|
||||
if (valueStr) {
|
||||
try {
|
||||
params = JSON.parse(valueStr);
|
||||
} catch {
|
||||
message.error({ content: '服务参数格式错误,请输入有效的JSON格式' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await sendDeviceMessage({
|
||||
deviceId: props.device.id!,
|
||||
method: IotDeviceMessageMethodEnum.SERVICE_INVOKE.method,
|
||||
params: {
|
||||
identifier: row.identifier,
|
||||
params,
|
||||
},
|
||||
});
|
||||
|
||||
message.success({ content: '服务调用成功' });
|
||||
// 延迟刷新设备消息列表
|
||||
deviceMessageRef.value?.refresh(deviceMessageRefreshDelay);
|
||||
} catch (error) {
|
||||
message.error({ content: '服务调用失败' });
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<Row :gutter="16">
|
||||
<!-- 左侧:指令调试区域 -->
|
||||
<Col :xs="24" :sm="24" :md="24" :lg="12" :xl="12">
|
||||
<Card class="simulator-tabs h-full">
|
||||
<template #title>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>指令调试</span>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
@click="debugCollapsed = !debugCollapsed"
|
||||
>
|
||||
<IconifyIcon icon="lucide:chevron-up" v-if="!debugCollapsed" />
|
||||
<IconifyIcon icon="lucide:chevron-down" v-if="debugCollapsed" />
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
<div v-show="!debugCollapsed">
|
||||
<Tabs v-model:active-key="activeTab" size="small">
|
||||
<!-- 上行指令调试 -->
|
||||
<Tabs.TabPane key="upstream" tab="上行指令调试">
|
||||
<Tabs
|
||||
v-if="activeTab === 'upstream'"
|
||||
v-model:active-key="upstreamTab"
|
||||
size="small"
|
||||
>
|
||||
<!-- 属性上报 -->
|
||||
<Tabs.TabPane
|
||||
:key="IotDeviceMessageMethodEnum.PROPERTY_POST.method"
|
||||
tab="属性上报"
|
||||
>
|
||||
<ContentWrap>
|
||||
<Table
|
||||
:data-source="propertyList"
|
||||
align="center"
|
||||
:columns="propertyColumns"
|
||||
:pagination="false"
|
||||
:scroll="{ x: 'max-content', y: 300 }"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'dataType'">
|
||||
{{ record.property?.dataType ?? '-' }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'dataDefinition'">
|
||||
<DataDefinition :data="record" />
|
||||
</template>
|
||||
<template v-else-if="column.key === 'value'">
|
||||
<Input
|
||||
:value="getFormValue(record.identifier)"
|
||||
@update:value="
|
||||
setFormValue(record.identifier, $event)
|
||||
"
|
||||
placeholder="输入值"
|
||||
size="small"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600">
|
||||
设置属性值后,点击「发送属性上报」按钮
|
||||
</span>
|
||||
<Button type="primary" @click="handlePropertyPost">
|
||||
发送属性上报
|
||||
</Button>
|
||||
</div>
|
||||
</ContentWrap>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<!-- 事件上报 -->
|
||||
<Tabs.TabPane
|
||||
:key="IotDeviceMessageMethodEnum.EVENT_POST.method"
|
||||
tab="事件上报"
|
||||
>
|
||||
<ContentWrap>
|
||||
<Table
|
||||
:data-source="eventList"
|
||||
align="center"
|
||||
:columns="eventColumns"
|
||||
:pagination="false"
|
||||
:scroll="{ x: 'max-content', y: 300 }"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'dataType'">
|
||||
{{ record.event?.dataType ?? '-' }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'dataDefinition'">
|
||||
<DataDefinition :data="record" />
|
||||
</template>
|
||||
<template v-else-if="column.key === 'value'">
|
||||
<Textarea
|
||||
:value="getFormValue(record.identifier)"
|
||||
@update:value="
|
||||
setFormValue(record.identifier, $event)
|
||||
"
|
||||
:rows="3"
|
||||
placeholder="输入事件参数(JSON格式)"
|
||||
size="small"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleEventPost(record)"
|
||||
>
|
||||
上报事件
|
||||
</Button>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
</ContentWrap>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<!-- 状态变更 -->
|
||||
<Tabs.TabPane
|
||||
:key="IotDeviceMessageMethodEnum.STATE_UPDATE.method"
|
||||
tab="状态变更"
|
||||
>
|
||||
<ContentWrap>
|
||||
<div class="flex gap-4">
|
||||
<Button
|
||||
type="primary"
|
||||
@click="handleDeviceState(DeviceStateEnum.ONLINE)"
|
||||
>
|
||||
设备上线
|
||||
</Button>
|
||||
<Button
|
||||
danger
|
||||
@click="handleDeviceState(DeviceStateEnum.OFFLINE)"
|
||||
>
|
||||
设备下线
|
||||
</Button>
|
||||
</div>
|
||||
</ContentWrap>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<!-- 下行指令调试 -->
|
||||
<Tabs.TabPane key="downstream" tab="下行指令调试">
|
||||
<Tabs
|
||||
v-if="activeTab === 'downstream'"
|
||||
v-model:active-key="downstreamTab"
|
||||
size="small"
|
||||
>
|
||||
<!-- 属性调试 -->
|
||||
<Tabs.TabPane
|
||||
:key="IotDeviceMessageMethodEnum.PROPERTY_SET.method"
|
||||
tab="属性设置"
|
||||
>
|
||||
<ContentWrap>
|
||||
<Table
|
||||
:data-source="propertyList"
|
||||
align="center"
|
||||
:columns="propertyColumns"
|
||||
:pagination="false"
|
||||
:scroll="{ x: 'max-content', y: 300 }"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'dataType'">
|
||||
{{ record.property?.dataType ?? '-' }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'dataDefinition'">
|
||||
<DataDefinition :data="record" />
|
||||
</template>
|
||||
<template v-else-if="column.key === 'value'">
|
||||
<Input
|
||||
:value="getFormValue(record.identifier)"
|
||||
@update:value="
|
||||
setFormValue(record.identifier, $event)
|
||||
"
|
||||
placeholder="输入值"
|
||||
size="small"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600">
|
||||
设置属性值后,点击「发送属性设置」按钮
|
||||
</span>
|
||||
<Button type="primary" @click="handlePropertySet">
|
||||
发送属性设置
|
||||
</Button>
|
||||
</div>
|
||||
</ContentWrap>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<!-- 服务调用 -->
|
||||
<Tabs.TabPane
|
||||
:key="IotDeviceMessageMethodEnum.SERVICE_INVOKE.method"
|
||||
tab="设备服务调用"
|
||||
>
|
||||
<ContentWrap>
|
||||
<Table
|
||||
:data-source="serviceList"
|
||||
align="center"
|
||||
:columns="serviceColumns"
|
||||
:pagination="false"
|
||||
:scroll="{ x: 'max-content', y: 300 }"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'dataDefinition'">
|
||||
<DataDefinition :data="record" />
|
||||
</template>
|
||||
<template v-else-if="column.key === 'value'">
|
||||
<Textarea
|
||||
:value="getFormValue(record.identifier)"
|
||||
@update:value="
|
||||
setFormValue(record.identifier, $event)
|
||||
"
|
||||
:rows="3"
|
||||
placeholder="输入服务参数(JSON格式)"
|
||||
size="small"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleServiceInvoke(record)"
|
||||
>
|
||||
服务调用
|
||||
</Button>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
</ContentWrap>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<!-- 右侧:设备消息区域 -->
|
||||
<Col :xs="24" :sm="24" :md="24" :lg="12" :xl="12">
|
||||
<Card class="h-full">
|
||||
<template #title>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>设备消息</span>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
@click="messageCollapsed = !messageCollapsed"
|
||||
>
|
||||
<IconifyIcon icon="lucide:chevron-down" v-if="!messageCollapsed" />
|
||||
<IconifyIcon icon="lucide:chevron-down" v-if="messageCollapsed" />
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
<div v-show="!messageCollapsed">
|
||||
<DeviceDetailsMessage
|
||||
v-if="device.id"
|
||||
ref="deviceMessageRef"
|
||||
:device-id="device.id"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
@@ -0,0 +1,264 @@
|
||||
<!-- 设备事件管理 -->
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { ThingModelData } from '#/api/iot/thingmodel';
|
||||
|
||||
import { computed, onMounted, reactive, watch } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import {
|
||||
Button,
|
||||
RangePicker,
|
||||
Select,
|
||||
Space,
|
||||
Tag,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getDeviceMessagePairPage } from '#/api/iot/device/device';
|
||||
import {
|
||||
getEventTypeLabel,
|
||||
IotDeviceMessageMethodEnum,
|
||||
IoTThingModelTypeEnum,
|
||||
} from '#/views/iot/utils/constants';
|
||||
|
||||
const props = defineProps<{
|
||||
deviceId: number;
|
||||
thingModelList: ThingModelData[];
|
||||
}>();
|
||||
|
||||
/** 查询参数 */
|
||||
const queryParams = reactive({
|
||||
identifier: '',
|
||||
times: undefined as [string, string] | undefined,
|
||||
});
|
||||
|
||||
/** 事件类型的物模型数据 */
|
||||
const eventThingModels = computed(() => {
|
||||
return props.thingModelList.filter(
|
||||
(item: ThingModelData) =>
|
||||
String(item.type) === String(IoTThingModelTypeEnum.EVENT),
|
||||
);
|
||||
});
|
||||
|
||||
/** Grid 列定义 */
|
||||
function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'reportTime',
|
||||
title: '上报时间',
|
||||
width: 180,
|
||||
slots: { default: 'reportTime' },
|
||||
},
|
||||
{
|
||||
field: 'identifier',
|
||||
title: '标识符',
|
||||
width: 160,
|
||||
slots: { default: 'identifier' },
|
||||
},
|
||||
{
|
||||
field: 'eventName',
|
||||
title: '事件名称',
|
||||
width: 160,
|
||||
slots: { default: 'eventName' },
|
||||
},
|
||||
{
|
||||
field: 'eventType',
|
||||
title: '事件类型',
|
||||
width: 100,
|
||||
slots: { default: 'eventType' },
|
||||
},
|
||||
{
|
||||
field: 'params',
|
||||
title: '输入参数',
|
||||
minWidth: 200,
|
||||
showOverflow: 'tooltip',
|
||||
slots: { default: 'params' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 创建 Grid 实例 */
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }) => {
|
||||
if (!props.deviceId) {
|
||||
return { list: [], total: 0 };
|
||||
}
|
||||
return await getDeviceMessagePairPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
deviceId: props.deviceId,
|
||||
method: IotDeviceMessageMethodEnum.EVENT_POST.method,
|
||||
identifier: queryParams.identifier || undefined,
|
||||
times: queryParams.times,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: false,
|
||||
search: false,
|
||||
},
|
||||
pagerConfig: {
|
||||
enabled: true,
|
||||
},
|
||||
} as VxeTableGridOptions,
|
||||
});
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function resetQuery() {
|
||||
queryParams.identifier = '';
|
||||
queryParams.times = undefined;
|
||||
handleQuery();
|
||||
}
|
||||
|
||||
/** 获取事件名称 */
|
||||
function getEventName(identifier: string | undefined) {
|
||||
if (!identifier) return '-';
|
||||
const event = eventThingModels.value.find(
|
||||
(item: ThingModelData) => item.identifier === identifier,
|
||||
);
|
||||
return event?.name || identifier;
|
||||
}
|
||||
|
||||
/** 获取事件类型 */
|
||||
function getEventType(identifier: string | undefined) {
|
||||
if (!identifier) return '-';
|
||||
const event = eventThingModels.value.find(
|
||||
(item: ThingModelData) => item.identifier === identifier,
|
||||
);
|
||||
if (!event?.event?.type) return '-';
|
||||
return getEventTypeLabel(event.event.type) || '-';
|
||||
}
|
||||
|
||||
/** 解析参数 */
|
||||
function parseParams(params: string) {
|
||||
try {
|
||||
const parsed = JSON.parse(params);
|
||||
if (parsed.params) {
|
||||
return parsed.params;
|
||||
}
|
||||
return parsed;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/** 刷新列表 */
|
||||
function refresh(delay = 0) {
|
||||
if (delay > 0) {
|
||||
setTimeout(() => gridApi.query(), delay);
|
||||
} else {
|
||||
gridApi.query();
|
||||
}
|
||||
}
|
||||
|
||||
/** 监听设备标识变化 */
|
||||
watch(
|
||||
() => props.deviceId,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
handleQuery();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
if (props.deviceId) {
|
||||
handleQuery();
|
||||
}
|
||||
});
|
||||
|
||||
/** 暴露方法给父组件 */
|
||||
defineExpose({
|
||||
refresh,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<!-- 搜索区域 -->
|
||||
<div class="mb-4 flex flex-wrap items-center gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<span>标识符:</span>
|
||||
<Select
|
||||
v-model:value="queryParams.identifier"
|
||||
placeholder="请选择事件标识符"
|
||||
allow-clear
|
||||
style="width: 240px"
|
||||
>
|
||||
<Select.Option
|
||||
v-for="event in eventThingModels"
|
||||
:key="event.identifier"
|
||||
:value="event.identifier!"
|
||||
>
|
||||
{{ event.name }}({{ event.identifier }})
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>时间范围:</span>
|
||||
<RangePicker
|
||||
v-model:value="queryParams.times"
|
||||
show-time
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
style="width: 360px"
|
||||
/>
|
||||
</div>
|
||||
<Space>
|
||||
<Button type="primary" @click="handleQuery">
|
||||
<template #icon>
|
||||
<IconifyIcon icon="ep:search" />
|
||||
</template>
|
||||
搜索
|
||||
</Button>
|
||||
<Button @click="resetQuery">
|
||||
<template #icon>
|
||||
<IconifyIcon icon="ep:refresh" />
|
||||
</template>
|
||||
重置
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<!-- 事件列表 -->
|
||||
<Grid>
|
||||
<template #reportTime="{ row }">
|
||||
{{
|
||||
row.request?.reportTime
|
||||
? formatDateTime(row.request.reportTime)
|
||||
: '-'
|
||||
}}
|
||||
</template>
|
||||
<template #identifier="{ row }">
|
||||
<Tag color="blue" size="small">
|
||||
{{ row.request?.identifier }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template #eventName="{ row }">
|
||||
{{ getEventName(row.request?.identifier) }}
|
||||
</template>
|
||||
<template #eventType="{ row }">
|
||||
{{ getEventType(row.request?.identifier) }}
|
||||
</template>
|
||||
<template #params="{ row }">
|
||||
{{ parseParams(row.request?.params) }}
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -57,32 +57,54 @@ const queryParams = reactive({
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
/** 不支持图表展示的数据类型列表 */
|
||||
const CHART_DISABLED_DATA_TYPES = [
|
||||
IoTDataSpecsDataTypeEnum.ARRAY, // 数组
|
||||
IoTDataSpecsDataTypeEnum.STRUCT, // 结构体
|
||||
IoTDataSpecsDataTypeEnum.TEXT, // 文本型
|
||||
IoTDataSpecsDataTypeEnum.BOOL, // 布尔型
|
||||
IoTDataSpecsDataTypeEnum.ENUM, // 枚举型
|
||||
IoTDataSpecsDataTypeEnum.DATE, // 时间型
|
||||
] as const;
|
||||
|
||||
/** 判断是否支持图表展示(仅数值类型支持:int、float、double) */
|
||||
const canShowChart = computed(() => {
|
||||
if (!thingModelDataType.value) return false;
|
||||
return !CHART_DISABLED_DATA_TYPES.includes(
|
||||
thingModelDataType.value as (typeof CHART_DISABLED_DATA_TYPES)[number],
|
||||
);
|
||||
});
|
||||
|
||||
/** 判断是否为复杂数据类型(用于格式化显示) */
|
||||
const isComplexDataType = computed(() => {
|
||||
if (!thingModelDataType.value) return false;
|
||||
return [
|
||||
IoTDataSpecsDataTypeEnum.ARRAY,
|
||||
IoTDataSpecsDataTypeEnum.STRUCT,
|
||||
].includes(thingModelDataType.value as any);
|
||||
}); // 判断是否为复杂数据类型(struct 或 array)
|
||||
return (
|
||||
thingModelDataType.value === IoTDataSpecsDataTypeEnum.ARRAY ||
|
||||
thingModelDataType.value === IoTDataSpecsDataTypeEnum.STRUCT
|
||||
);
|
||||
});
|
||||
|
||||
/** 最大值统计 */
|
||||
const maxValue = computed(() => {
|
||||
if (isComplexDataType.value || list.value.length === 0) return '-';
|
||||
if (!canShowChart.value || list.value.length === 0) return '-';
|
||||
const values = list.value
|
||||
.map((item) => Number(item.value))
|
||||
.filter((v) => !Number.isNaN(v));
|
||||
return values.length > 0 ? Math.max(...values).toFixed(2) : '-';
|
||||
}); // 统计数据
|
||||
});
|
||||
|
||||
/** 最小值统计 */
|
||||
const minValue = computed(() => {
|
||||
if (isComplexDataType.value || list.value.length === 0) return '-';
|
||||
if (!canShowChart.value || list.value.length === 0) return '-';
|
||||
const values = list.value
|
||||
.map((item) => Number(item.value))
|
||||
.filter((v) => !Number.isNaN(v));
|
||||
return values.length > 0 ? Math.min(...values).toFixed(2) : '-';
|
||||
});
|
||||
|
||||
/** 平均值统计 */
|
||||
const avgValue = computed(() => {
|
||||
if (isComplexDataType.value || list.value.length === 0) return '-';
|
||||
if (!canShowChart.value || list.value.length === 0) return '-';
|
||||
const values = list.value
|
||||
.map((item) => Number(item.value))
|
||||
.filter((v) => !Number.isNaN(v));
|
||||
@@ -142,10 +164,10 @@ async function getList() {
|
||||
) as IotDeviceApi.DevicePropertyDetail[];
|
||||
total.value = list.value.length;
|
||||
|
||||
// 如果是图表模式且不是复杂数据类型,等待渲染图表
|
||||
// 如果是图表模式且支持图表展示,等待渲染图表
|
||||
if (
|
||||
viewMode.value === 'chart' &&
|
||||
!isComplexDataType.value &&
|
||||
canShowChart.value &&
|
||||
list.value.length > 0
|
||||
) {
|
||||
await renderChartWhenReady();
|
||||
@@ -287,8 +309,8 @@ async function open(deviceId: number, identifier: string, dataType: string) {
|
||||
// 更新查询参数的时间
|
||||
queryParams.times = formatDateRangeWithTime(dateRange.value);
|
||||
|
||||
// 如果物模型是 struct、array,需要默认使用 list 模式
|
||||
viewMode.value = isComplexDataType.value ? 'list' : 'chart';
|
||||
// 如果不支持图表展示,默认使用列表模式
|
||||
viewMode.value = canShowChart.value ? 'chart' : 'list';
|
||||
|
||||
// 等待弹窗完全渲染后再获取数据
|
||||
await nextTick();
|
||||
@@ -380,11 +402,7 @@ function formatComplexValue(value: any) {
|
||||
|
||||
/** 监听视图模式变化,重新渲染图表 */
|
||||
watch(viewMode, async (newMode) => {
|
||||
if (
|
||||
newMode === 'chart' &&
|
||||
!isComplexDataType.value &&
|
||||
list.value.length > 0
|
||||
) {
|
||||
if (newMode === 'chart' && canShowChart.value && list.value.length > 0) {
|
||||
await renderChartWhenReady();
|
||||
}
|
||||
});
|
||||
@@ -396,7 +414,6 @@ defineExpose({ open }); // 提供 open 方法,用于打开弹窗
|
||||
v-model:open="dialogVisible"
|
||||
title="查看数据"
|
||||
width="1200px"
|
||||
:destroy-on-close="true"
|
||||
@cancel="handleClose"
|
||||
>
|
||||
<div class="property-history-container">
|
||||
@@ -436,7 +453,7 @@ defineExpose({ open }); // 提供 open 方法,用于打开弹窗
|
||||
<Button
|
||||
:type="viewMode === 'chart' ? 'primary' : 'default'"
|
||||
@click="viewMode = 'chart'"
|
||||
:disabled="isComplexDataType"
|
||||
:disabled="!canShowChart"
|
||||
>
|
||||
<template #icon>
|
||||
<IconifyIcon icon="ant-design:line-chart-outlined" />
|
||||
@@ -459,7 +476,7 @@ defineExpose({ open }); // 提供 open 方法,用于打开弹窗
|
||||
<div v-if="list.length > 0" class="mt-3 text-sm text-gray-600">
|
||||
<Space :size="16">
|
||||
<span>共 {{ total }} 条数据</span>
|
||||
<span v-if="viewMode === 'chart' && !isComplexDataType">
|
||||
<span v-if="viewMode === 'chart' && canShowChart">
|
||||
最大值: {{ maxValue }} | 最小值: {{ minValue }} | 平均值:
|
||||
{{ avgValue }}
|
||||
</span>
|
||||
@@ -529,10 +546,7 @@ defineExpose({ open }); // 提供 open 方法,用于打开弹窗
|
||||
.chart-container,
|
||||
.table-container {
|
||||
padding: 16px;
|
||||
background-color: hsl(
|
||||
var(--card)
|
||||
); // TODO @haohao:看看这个能不能 fix 下~ idea 爆红了;
|
||||
|
||||
background-color: hsl(var(--card) / 100%);
|
||||
border: 1px solid hsl(var(--border) / 60%);
|
||||
border-radius: 8px;
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
<!-- 设备属性管理 -->
|
||||
<script lang="ts" setup>
|
||||
// TODO @haohao:看看能不能用 Grid 实现下,方便 element-plus 迁移
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { IotDeviceApi } from '#/api/iot/device/device';
|
||||
|
||||
import { onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
|
||||
|
||||
import { ContentWrap } from '@vben/common-ui';
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { formatDate } from '@vben/utils';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import {
|
||||
Button,
|
||||
@@ -17,37 +17,168 @@ import {
|
||||
Input,
|
||||
Row,
|
||||
Switch,
|
||||
Table,
|
||||
Tag,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getLatestDeviceProperties } from '#/api/iot/device/device';
|
||||
|
||||
import DeviceDetailsThingModelPropertyHistory from './thing-model-property-history.vue';
|
||||
|
||||
const props = defineProps<{ deviceId: number }>();
|
||||
|
||||
const loading = ref(true); // 列表的加载中
|
||||
const list = ref<IotDeviceApi.DevicePropertyDetail[]>([]); // 显示的列表数据
|
||||
const filterList = ref<IotDeviceApi.DevicePropertyDetail[]>([]); // 完整的数据列表
|
||||
/** 列表的加载中 */
|
||||
const loading = ref(true);
|
||||
/** 显示的列表数据 */
|
||||
const list = ref<IotDeviceApi.DevicePropertyDetail[]>([]);
|
||||
/** 完整的数据列表 */
|
||||
const filterList = ref<IotDeviceApi.DevicePropertyDetail[]>([]);
|
||||
/** 查询参数 */
|
||||
const queryParams = reactive({
|
||||
keyword: '' as string,
|
||||
});
|
||||
const autoRefresh = ref(false); // 自动刷新开关
|
||||
let autoRefreshTimer: any = null; // 定时器
|
||||
const viewMode = ref<'card' | 'list'>('card'); // 视图模式状态
|
||||
/** 自动刷新开关 */
|
||||
const autoRefresh = ref(false);
|
||||
/** 定时器 */
|
||||
let autoRefreshTimer: any = null;
|
||||
/** 视图模式状态 */
|
||||
const viewMode = ref<'card' | 'list'>('card');
|
||||
|
||||
/** Grid 列定义 */
|
||||
function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'identifier',
|
||||
title: '属性标识符',
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '属性名称',
|
||||
},
|
||||
{
|
||||
field: 'dataType',
|
||||
title: '数据类型',
|
||||
},
|
||||
{
|
||||
field: 'value',
|
||||
title: '属性值',
|
||||
slots: { default: 'value' },
|
||||
},
|
||||
{
|
||||
field: 'updateTime',
|
||||
title: '更新时间',
|
||||
width: 180,
|
||||
slots: { default: 'updateTime' },
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 120,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 创建 Grid 实例 */
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
rowConfig: {
|
||||
keyField: 'identifier',
|
||||
isHover: true,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async () => {
|
||||
if (!props.deviceId) {
|
||||
return { list: [], total: 0 };
|
||||
}
|
||||
const data = await getLatestDeviceProperties({
|
||||
deviceId: props.deviceId,
|
||||
identifier: undefined,
|
||||
name: undefined,
|
||||
});
|
||||
// 筛选数据
|
||||
let filteredData = data;
|
||||
if (queryParams.keyword.trim()) {
|
||||
const keyword = queryParams.keyword.toLowerCase();
|
||||
filteredData = data.filter(
|
||||
(item: IotDeviceApi.DevicePropertyDetail) =>
|
||||
item.identifier?.toLowerCase().includes(keyword) ||
|
||||
item.name?.toLowerCase().includes(keyword),
|
||||
);
|
||||
}
|
||||
// 更新本地列表用于卡片视图
|
||||
filterList.value = data;
|
||||
list.value = filteredData;
|
||||
return {
|
||||
list: filteredData,
|
||||
total: filteredData.length,
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: false,
|
||||
search: false,
|
||||
},
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
} as VxeTableGridOptions<IotDeviceApi.DevicePropertyDetail>,
|
||||
});
|
||||
|
||||
// 包装 gridApi.query() 方法,统一列表视图和卡片视图的查询接口
|
||||
gridApi.query = async () => {
|
||||
if (viewMode.value === 'list') {
|
||||
// 列表视图:手动获取数据并加载到 Grid
|
||||
if (!props.deviceId) {
|
||||
return;
|
||||
}
|
||||
const data = await getLatestDeviceProperties({
|
||||
deviceId: props.deviceId,
|
||||
identifier: undefined,
|
||||
name: undefined,
|
||||
});
|
||||
const dataArray = Array.isArray(data) ? data : [];
|
||||
let filteredData = dataArray;
|
||||
if (queryParams.keyword.trim()) {
|
||||
const keyword = queryParams.keyword.toLowerCase();
|
||||
filteredData = dataArray.filter(
|
||||
(item: IotDeviceApi.DevicePropertyDetail) =>
|
||||
item.identifier?.toLowerCase().includes(keyword) ||
|
||||
item.name?.toLowerCase().includes(keyword),
|
||||
);
|
||||
}
|
||||
filterList.value = dataArray;
|
||||
list.value = filteredData;
|
||||
// 直接加载数据到 Grid
|
||||
if (gridApi.grid) {
|
||||
gridApi.grid.loadData(filteredData);
|
||||
}
|
||||
} else {
|
||||
// 卡片视图:调用 getList 方法
|
||||
await getList();
|
||||
}
|
||||
};
|
||||
|
||||
/** 查询列表 */
|
||||
async function getList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params = {
|
||||
deviceId: props.deviceId,
|
||||
identifier: undefined as string | undefined,
|
||||
name: undefined as string | undefined,
|
||||
};
|
||||
filterList.value = await getLatestDeviceProperties(params);
|
||||
handleFilter();
|
||||
if (viewMode.value === 'list') {
|
||||
await gridApi.query();
|
||||
} else {
|
||||
// 卡片视图:手动获取数据
|
||||
const params = {
|
||||
deviceId: props.deviceId,
|
||||
identifier: undefined as string | undefined,
|
||||
name: undefined as string | undefined,
|
||||
};
|
||||
filterList.value = await getLatestDeviceProperties(params);
|
||||
handleFilter();
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -69,7 +200,21 @@ function handleFilter() {
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery() {
|
||||
handleFilter();
|
||||
if (viewMode.value === 'list') {
|
||||
gridApi.query();
|
||||
} else {
|
||||
handleFilter();
|
||||
}
|
||||
}
|
||||
|
||||
/** 视图切换 */
|
||||
async function handleViewModeChange(mode: 'card' | 'list') {
|
||||
if (viewMode.value === mode) {
|
||||
return;
|
||||
}
|
||||
viewMode.value = mode;
|
||||
await nextTick();
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 历史操作 */
|
||||
@@ -91,29 +236,43 @@ function formatValueWithUnit(item: IotDeviceApi.DevicePropertyDetail) {
|
||||
watch(autoRefresh, (newValue) => {
|
||||
if (newValue) {
|
||||
autoRefreshTimer = setInterval(() => {
|
||||
getList();
|
||||
}, 5000); // 每 5 秒刷新一次
|
||||
gridApi.query();
|
||||
}, 5000);
|
||||
} else {
|
||||
clearInterval(autoRefreshTimer);
|
||||
autoRefreshTimer = null;
|
||||
}
|
||||
});
|
||||
|
||||
/** 监听设备标识变化 */
|
||||
watch(
|
||||
() => props.deviceId,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
gridApi.query();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
if (props.deviceId) {
|
||||
await nextTick();
|
||||
gridApi.query();
|
||||
}
|
||||
});
|
||||
|
||||
/** 组件卸载时清除定时器 */
|
||||
onBeforeUnmount(() => {
|
||||
if (autoRefreshTimer) {
|
||||
clearInterval(autoRefreshTimer);
|
||||
autoRefreshTimer = null;
|
||||
}
|
||||
});
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<Page auto-content-height>
|
||||
<!-- 搜索工作栏 -->
|
||||
<div class="flex items-center justify-between" style="margin-bottom: 16px">
|
||||
<div class="flex items-center" style="gap: 16px">
|
||||
@@ -134,13 +293,13 @@ onMounted(() => {
|
||||
<Button.Group>
|
||||
<Button
|
||||
:type="viewMode === 'card' ? 'primary' : 'default'"
|
||||
@click="viewMode = 'card'"
|
||||
@click="handleViewModeChange('card')"
|
||||
>
|
||||
<IconifyIcon icon="ep:grid" />
|
||||
</Button>
|
||||
<Button
|
||||
:type="viewMode === 'list' ? 'primary' : 'default'"
|
||||
@click="viewMode = 'list'"
|
||||
@click="handleViewModeChange('list')"
|
||||
>
|
||||
<IconifyIcon icon="ep:list" />
|
||||
</Button>
|
||||
@@ -214,7 +373,7 @@ onMounted(() => {
|
||||
<div class="mb-2.5 last:mb-0">
|
||||
<span class="mr-2.5 text-muted-foreground">更新时间</span>
|
||||
<span class="text-sm text-foreground">
|
||||
{{ item.updateTime ? formatDate(item.updateTime) : '-' }}
|
||||
{{ item.updateTime ? formatDateTime(item.updateTime) : '-' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -225,45 +384,31 @@ onMounted(() => {
|
||||
</template>
|
||||
|
||||
<!-- 列表视图 -->
|
||||
<Table v-else v-loading="loading" :data-source="list" :pagination="false">
|
||||
<Table.Column title="属性标识符" align="center" data-index="identifier" />
|
||||
<Table.Column title="属性名称" align="center" data-index="name" />
|
||||
<Table.Column title="数据类型" align="center" data-index="dataType" />
|
||||
<Table.Column title="属性值" align="center" data-index="value">
|
||||
<template #default="{ record }">
|
||||
{{ formatValueWithUnit(record) }}
|
||||
</template>
|
||||
</Table.Column>
|
||||
<Table.Column
|
||||
title="更新时间"
|
||||
align="center"
|
||||
data-index="updateTime"
|
||||
:width="180"
|
||||
>
|
||||
<template #default="{ record }">
|
||||
{{ record.updateTime ? formatDate(record.updateTime) : '-' }}
|
||||
</template>
|
||||
</Table.Column>
|
||||
<Table.Column title="操作" align="center">
|
||||
<template #default="{ record }">
|
||||
<Button
|
||||
type="link"
|
||||
@click="
|
||||
openHistory(props.deviceId, record.identifier, record.dataType)
|
||||
"
|
||||
>
|
||||
查看数据
|
||||
</Button>
|
||||
</template>
|
||||
</Table.Column>
|
||||
</Table>
|
||||
<Grid v-show="viewMode === 'list'">
|
||||
<template #value="{ row }">
|
||||
{{ formatValueWithUnit(row) }}
|
||||
</template>
|
||||
<template #updateTime="{ row }">
|
||||
{{ row.updateTime ? formatDateTime(row.updateTime) : '-' }}
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<Button
|
||||
type="link"
|
||||
@click="
|
||||
openHistory(props.deviceId, row.identifier, row.dataType)
|
||||
"
|
||||
>
|
||||
查看数据
|
||||
</Button>
|
||||
</template>
|
||||
</Grid>
|
||||
|
||||
<!-- 表单弹窗:添加/修改 -->
|
||||
<DeviceDetailsThingModelPropertyHistory
|
||||
ref="historyRef"
|
||||
:device-id="props.deviceId"
|
||||
/>
|
||||
</ContentWrap>
|
||||
</Page>
|
||||
</template>
|
||||
<style scoped>
|
||||
/* 移除 a-row 的额外边距 */
|
||||
@@ -0,0 +1,291 @@
|
||||
<!-- 设备服务调用 -->
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { ThingModelData } from '#/api/iot/thingmodel';
|
||||
|
||||
import { computed, onMounted, reactive, watch } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import {
|
||||
Button,
|
||||
RangePicker,
|
||||
Select,
|
||||
Space,
|
||||
Tag,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getDeviceMessagePairPage } from '#/api/iot/device/device';
|
||||
import {
|
||||
getThingModelServiceCallTypeLabel,
|
||||
IotDeviceMessageMethodEnum,
|
||||
IoTThingModelTypeEnum,
|
||||
} from '#/views/iot/utils/constants';
|
||||
|
||||
const props = defineProps<{
|
||||
deviceId: number;
|
||||
thingModelList: ThingModelData[];
|
||||
}>();
|
||||
|
||||
/** 查询参数 */
|
||||
const queryParams = reactive({
|
||||
identifier: '',
|
||||
times: undefined as [string, string] | undefined,
|
||||
});
|
||||
|
||||
/** 服务类型的物模型数据 */
|
||||
const serviceThingModels = computed(() => {
|
||||
return props.thingModelList.filter(
|
||||
(item: ThingModelData) =>
|
||||
String(item.type) === String(IoTThingModelTypeEnum.SERVICE),
|
||||
);
|
||||
});
|
||||
|
||||
/** Grid 列定义 */
|
||||
function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'requestTime',
|
||||
title: '调用时间',
|
||||
width: 180,
|
||||
slots: { default: 'requestTime' },
|
||||
},
|
||||
{
|
||||
field: 'responseTime',
|
||||
title: '响应时间',
|
||||
width: 180,
|
||||
slots: { default: 'responseTime' },
|
||||
},
|
||||
{
|
||||
field: 'identifier',
|
||||
title: '标识符',
|
||||
width: 160,
|
||||
slots: { default: 'identifier' },
|
||||
},
|
||||
{
|
||||
field: 'serviceName',
|
||||
title: '服务名称',
|
||||
width: 160,
|
||||
slots: { default: 'serviceName' },
|
||||
},
|
||||
{
|
||||
field: 'callType',
|
||||
title: '调用方式',
|
||||
width: 100,
|
||||
slots: { default: 'callType' },
|
||||
},
|
||||
{
|
||||
field: 'inputParams',
|
||||
title: '输入参数',
|
||||
minWidth: 200,
|
||||
showOverflow: 'tooltip',
|
||||
slots: { default: 'inputParams' },
|
||||
},
|
||||
{
|
||||
field: 'outputParams',
|
||||
title: '输出参数',
|
||||
minWidth: 200,
|
||||
showOverflow: 'tooltip',
|
||||
slots: { default: 'outputParams' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 创建 Grid 实例 */
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }) => {
|
||||
if (!props.deviceId) {
|
||||
return { list: [], total: 0 };
|
||||
}
|
||||
return await getDeviceMessagePairPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
deviceId: props.deviceId,
|
||||
method: IotDeviceMessageMethodEnum.SERVICE_INVOKE.method,
|
||||
identifier: queryParams.identifier || undefined,
|
||||
times: queryParams.times,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: false,
|
||||
search: false,
|
||||
},
|
||||
pagerConfig: {
|
||||
enabled: true,
|
||||
},
|
||||
} as VxeTableGridOptions,
|
||||
});
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function resetQuery() {
|
||||
queryParams.identifier = '';
|
||||
queryParams.times = undefined;
|
||||
handleQuery();
|
||||
}
|
||||
|
||||
/** 获取服务名称 */
|
||||
function getServiceName(identifier: string | undefined) {
|
||||
if (!identifier) return '-';
|
||||
const service = serviceThingModels.value.find(
|
||||
(item: ThingModelData) => item.identifier === identifier,
|
||||
);
|
||||
return service?.name || identifier;
|
||||
}
|
||||
|
||||
/** 获取调用方式 */
|
||||
function getCallType(identifier: string | undefined) {
|
||||
if (!identifier) return '-';
|
||||
const service = serviceThingModels.value.find(
|
||||
(item: ThingModelData) => item.identifier === identifier,
|
||||
);
|
||||
if (!service?.service?.callType) return '-';
|
||||
return getThingModelServiceCallTypeLabel(service.service.callType) || '-';
|
||||
}
|
||||
|
||||
/** 解析参数 */
|
||||
function parseParams(params: string) {
|
||||
if (!params) return '-';
|
||||
try {
|
||||
const parsed = JSON.parse(params);
|
||||
if (parsed.params) {
|
||||
return JSON.stringify(parsed.params, null, 2);
|
||||
}
|
||||
return JSON.stringify(parsed, null, 2);
|
||||
} catch {
|
||||
return params;
|
||||
}
|
||||
}
|
||||
|
||||
/** 刷新列表 */
|
||||
function refresh(delay = 0) {
|
||||
if (delay > 0) {
|
||||
setTimeout(() => gridApi.query(), delay);
|
||||
} else {
|
||||
gridApi.query();
|
||||
}
|
||||
}
|
||||
|
||||
/** 监听设备标识变化 */
|
||||
watch(
|
||||
() => props.deviceId,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
handleQuery();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
if (props.deviceId) {
|
||||
handleQuery();
|
||||
}
|
||||
});
|
||||
|
||||
/** 暴露方法给父组件 */
|
||||
defineExpose({
|
||||
refresh,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<!-- 搜索区域 -->
|
||||
<div class="mb-4 flex flex-wrap items-center gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<span>标识符:</span>
|
||||
<Select
|
||||
v-model:value="queryParams.identifier"
|
||||
placeholder="请选择服务标识符"
|
||||
allow-clear
|
||||
style="width: 240px"
|
||||
>
|
||||
<Select.Option
|
||||
v-for="service in serviceThingModels"
|
||||
:key="service.identifier"
|
||||
:value="service.identifier!"
|
||||
>
|
||||
{{ service.name }}({{ service.identifier }})
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>时间范围:</span>
|
||||
<RangePicker
|
||||
v-model:value="queryParams.times"
|
||||
show-time
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
style="width: 360px"
|
||||
/>
|
||||
</div>
|
||||
<Space>
|
||||
<Button type="primary" @click="handleQuery">
|
||||
<template #icon>
|
||||
<IconifyIcon icon="ep:search" />
|
||||
</template>
|
||||
搜索
|
||||
</Button>
|
||||
<Button @click="resetQuery">
|
||||
<template #icon>
|
||||
<IconifyIcon icon="ep:refresh" />
|
||||
</template>
|
||||
重置
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<!-- 服务调用列表 -->
|
||||
<Grid>
|
||||
<template #requestTime="{ row }">
|
||||
{{
|
||||
row.request?.reportTime
|
||||
? formatDateTime(row.request.reportTime)
|
||||
: '-'
|
||||
}}
|
||||
</template>
|
||||
<template #responseTime="{ row }">
|
||||
{{
|
||||
row.reply?.reportTime ? formatDateTime(row.reply.reportTime) : '-'
|
||||
}}
|
||||
</template>
|
||||
<template #identifier="{ row }">
|
||||
<Tag color="blue" size="small">
|
||||
{{ row.request?.identifier }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template #serviceName="{ row }">
|
||||
{{ getServiceName(row.request?.identifier) }}
|
||||
</template>
|
||||
<template #callType="{ row }">
|
||||
{{ getCallType(row.request?.identifier) }}
|
||||
</template>
|
||||
<template #inputParams="{ row }">
|
||||
{{ parseParams(row.request?.params) }}
|
||||
</template>
|
||||
<template #outputParams="{ row }">
|
||||
<span v-if="row.reply">
|
||||
{{
|
||||
`{"code":${row.reply.code},"msg":"${row.reply.msg}","data":${row.reply.data}\}`
|
||||
}}
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -2,14 +2,14 @@
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { IotDeviceApi } from '#/api/iot/device/device';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { nextTick, onMounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
import { downloadFileFromBlobPart, isEmpty } from '@vben/utils';
|
||||
|
||||
import {
|
||||
Button,
|
||||
@@ -72,24 +72,16 @@ const queryParams = ref({
|
||||
groupId: undefined as number | undefined,
|
||||
}); // 搜索参数
|
||||
|
||||
// 获取字典选项
|
||||
// TODO @haohao:直接使用 getDictOptions 哈,不用包装方法;
|
||||
const getIntDictOptions = (dictType: string) => {
|
||||
return getDictOptions(dictType, 'number');
|
||||
};
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
if (viewMode.value === 'list') {
|
||||
gridApi.formApi.setValues(queryParams.value);
|
||||
gridApi.query();
|
||||
} else {
|
||||
// todo @haohao:改成 query 方法,更统一;
|
||||
cardViewRef.value?.search(queryParams.value);
|
||||
}
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
/** 重置搜索 */
|
||||
function handleReset() {
|
||||
queryParams.value.deviceName = '';
|
||||
queryParams.value.nickname = '';
|
||||
@@ -100,13 +92,20 @@ function handleReset() {
|
||||
handleSearch();
|
||||
}
|
||||
|
||||
/** 刷新 */
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
if (viewMode.value === 'list') {
|
||||
gridApi.query();
|
||||
} else {
|
||||
cardViewRef.value?.reload();
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 视图切换 */
|
||||
async function handleViewModeChange(mode: 'card' | 'list') {
|
||||
if (viewMode.value === mode) {
|
||||
return; // 如果已经是目标视图,不需要切换
|
||||
}
|
||||
viewMode.value = mode;
|
||||
// 等待视图更新后再触发查询
|
||||
await nextTick();
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 导出表格 */
|
||||
@@ -161,9 +160,7 @@ async function handleDelete(row: IotDeviceApi.Device) {
|
||||
|
||||
/** 批量删除设备 */
|
||||
async function handleDeleteBatch() {
|
||||
const checkedRows = (gridApi.grid?.getCheckboxRecords() ||
|
||||
[]) as IotDeviceApi.Device[];
|
||||
if (checkedRows.length === 0) {
|
||||
if (checkedIds.value.length === 0) {
|
||||
message.warning('请选择要删除的设备');
|
||||
return;
|
||||
}
|
||||
@@ -172,9 +169,9 @@ async function handleDeleteBatch() {
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
const ids = checkedRows.map((row) => row.id!);
|
||||
await deleteDeviceList(ids);
|
||||
await deleteDeviceList(checkedIds.value);
|
||||
message.success($t('ui.actionMessage.deleteSuccess'));
|
||||
checkedIds.value = [];
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading();
|
||||
@@ -183,14 +180,11 @@ async function handleDeleteBatch() {
|
||||
|
||||
/** 添加到分组 */
|
||||
function handleAddToGroup() {
|
||||
const checkedRows = (gridApi.grid?.getCheckboxRecords() ||
|
||||
[]) as IotDeviceApi.Device[];
|
||||
if (checkedRows.length === 0) {
|
||||
if (checkedIds.value.length === 0) {
|
||||
message.warning('请选择要添加到分组的设备');
|
||||
return;
|
||||
}
|
||||
const ids = checkedRows.map((row) => row.id!);
|
||||
deviceGroupFormModalApi.setData(ids).open();
|
||||
deviceGroupFormModalApi.setData(checkedIds.value).open();
|
||||
}
|
||||
|
||||
/** 设备导入 */
|
||||
@@ -198,6 +192,16 @@ function handleImport() {
|
||||
deviceImportFormModalApi.open();
|
||||
}
|
||||
|
||||
const checkedIds = ref<number[]>([]);
|
||||
function handleRowCheckboxChange({
|
||||
records,
|
||||
}: {
|
||||
records: IotDeviceApi.Device[];
|
||||
}) {
|
||||
checkedIds.value = records.map((item) => item.id!);
|
||||
}
|
||||
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
checkboxConfig: {
|
||||
@@ -227,8 +231,23 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<IotDeviceApi.Device>,
|
||||
gridEvents: {
|
||||
checkboxAll: handleRowCheckboxChange,
|
||||
checkboxChange: handleRowCheckboxChange,
|
||||
},
|
||||
});
|
||||
|
||||
// 包装 gridApi.query() 方法,统一列表视图和卡片视图的查询接口
|
||||
const originalQuery = gridApi.query.bind(gridApi);
|
||||
gridApi.query = async (params?: Record<string, any>) => {
|
||||
if (viewMode.value === 'list') {
|
||||
return await originalQuery(params);
|
||||
} else {
|
||||
// 卡片视图:调用卡片组件的 query 方法
|
||||
cardViewRef.value?.query();
|
||||
}
|
||||
};
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
// 获取产品列表
|
||||
@@ -291,7 +310,7 @@ onMounted(async () => {
|
||||
style="width: 200px"
|
||||
>
|
||||
<Select.Option
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE)"
|
||||
v-for="dict in getDictOptions(DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE, 'number')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
@@ -305,7 +324,7 @@ onMounted(async () => {
|
||||
style="width: 200px"
|
||||
>
|
||||
<Select.Option
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_DEVICE_STATE)"
|
||||
v-for="dict in getDictOptions(DICT_TYPE.IOT_DEVICE_STATE, 'number')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
@@ -361,23 +380,23 @@ onMounted(async () => {
|
||||
auth: ['iot:device:import'],
|
||||
onClick: handleImport,
|
||||
},
|
||||
// TODO @haohao:应该是选中后,才可用
|
||||
{
|
||||
label: '添加到分组',
|
||||
type: 'primary',
|
||||
icon: 'ant-design:folder-add-outlined',
|
||||
auth: ['iot:device:update'],
|
||||
ifShow: () => viewMode === 'list',
|
||||
disabled: isEmpty(checkedIds),
|
||||
onClick: handleAddToGroup,
|
||||
},
|
||||
// TODO @haohao:应该是选中后,才可用;然后,然后 danger 颜色;
|
||||
{
|
||||
label: '批量删除',
|
||||
type: 'primary',
|
||||
color: 'error',
|
||||
danger: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['iot:device:delete'],
|
||||
ifShow: () => viewMode === 'list',
|
||||
disabled: isEmpty(checkedIds),
|
||||
onClick: handleDeleteBatch,
|
||||
},
|
||||
]"
|
||||
@@ -387,13 +406,13 @@ onMounted(async () => {
|
||||
<Space :size="4">
|
||||
<Button
|
||||
:type="viewMode === 'card' ? 'primary' : 'default'"
|
||||
@click="viewMode = 'card'"
|
||||
@click="handleViewModeChange('card')"
|
||||
>
|
||||
<IconifyIcon icon="ant-design:appstore-outlined" />
|
||||
</Button>
|
||||
<Button
|
||||
:type="viewMode === 'list' ? 'primary' : 'default'"
|
||||
@click="viewMode = 'list'"
|
||||
@click="handleViewModeChange('list')"
|
||||
>
|
||||
<IconifyIcon icon="ant-design:unordered-list-outlined" />
|
||||
</Button>
|
||||
@@ -402,10 +421,6 @@ onMounted(async () => {
|
||||
</Card>
|
||||
|
||||
<Grid table-title="设备列表" v-show="viewMode === 'list'">
|
||||
<template #toolbar-tools>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<!-- 所属产品列 -->
|
||||
<template #product="{ row }">
|
||||
<a
|
||||
@@ -484,10 +499,6 @@ onMounted(async () => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.vxe-toolbar div) {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 隐藏 VxeGrid 自带的搜索表单区域 */
|
||||
:deep(.vxe-grid--form-wrapper) {
|
||||
display: none !important;
|
||||
|
||||
@@ -223,6 +223,10 @@ defineExpose({
|
||||
queryParams.value.pageNo = 1;
|
||||
getList();
|
||||
},
|
||||
query: () => {
|
||||
queryParams.value.pageNo = 1;
|
||||
getList();
|
||||
},
|
||||
});
|
||||
|
||||
/** 初始化 */
|
||||
|
||||
@@ -1,273 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
computed,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
reactive,
|
||||
ref,
|
||||
watch,
|
||||
} from 'vue';
|
||||
|
||||
import { ContentWrap } from '@vben/common-ui';
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { formatDate } from '@vben/utils';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
Pagination,
|
||||
Select,
|
||||
Switch,
|
||||
Table,
|
||||
Tag,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { getDeviceMessagePage } from '#/api/iot/device/device';
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { IotDeviceMessageMethodEnum } from '#/views/iot/utils/constants';
|
||||
|
||||
// TODO @haohao:看看能不能调整成 Grid 风格~方便 element-plus 的迁移
|
||||
|
||||
const props = defineProps<{
|
||||
deviceId: number;
|
||||
}>();
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
deviceId: props.deviceId,
|
||||
method: undefined,
|
||||
upstream: undefined,
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
// 列表数据
|
||||
const loading = ref(false);
|
||||
const total = ref(0);
|
||||
const list = ref<any[]>([]);
|
||||
const autoRefresh = ref(false); // 自动刷新开关
|
||||
let autoRefreshTimer: any = null; // 自动刷新定时器
|
||||
|
||||
// 消息方法选项
|
||||
const methodOptions = computed(() => {
|
||||
return Object.values(IotDeviceMessageMethodEnum).map((item) => ({
|
||||
label: item.name,
|
||||
value: item.method,
|
||||
}));
|
||||
});
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'ts',
|
||||
key: 'ts',
|
||||
align: 'center' as const,
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '上行/下行',
|
||||
dataIndex: 'upstream',
|
||||
key: 'upstream',
|
||||
align: 'center' as const,
|
||||
width: 140,
|
||||
},
|
||||
{
|
||||
title: '是否回复',
|
||||
dataIndex: 'reply',
|
||||
key: 'reply',
|
||||
align: 'center' as const,
|
||||
width: 140,
|
||||
},
|
||||
{
|
||||
title: '请求编号',
|
||||
dataIndex: 'requestId',
|
||||
key: 'requestId',
|
||||
align: 'center' as const,
|
||||
width: 300,
|
||||
},
|
||||
{
|
||||
title: '请求方法',
|
||||
dataIndex: 'method',
|
||||
key: 'method',
|
||||
align: 'center' as const,
|
||||
width: 140,
|
||||
},
|
||||
{
|
||||
title: '请求/响应数据',
|
||||
dataIndex: 'params',
|
||||
key: 'params',
|
||||
align: 'center' as const,
|
||||
ellipsis: true,
|
||||
},
|
||||
];
|
||||
|
||||
/** 查询消息列表 */
|
||||
async function getMessageList() {
|
||||
if (!props.deviceId) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await getDeviceMessagePage(queryParams);
|
||||
total.value = data.total;
|
||||
list.value = data.list;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索操作 */
|
||||
function handleQuery() {
|
||||
queryParams.pageNo = 1;
|
||||
getMessageList();
|
||||
}
|
||||
|
||||
/** 监听自动刷新 */
|
||||
watch(autoRefresh, (newValue) => {
|
||||
if (newValue) {
|
||||
autoRefreshTimer = setInterval(() => {
|
||||
getMessageList();
|
||||
}, 5000);
|
||||
} else {
|
||||
clearInterval(autoRefreshTimer);
|
||||
autoRefreshTimer = null;
|
||||
}
|
||||
});
|
||||
|
||||
/** 监听设备标识变化 */
|
||||
watch(
|
||||
() => props.deviceId,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
handleQuery();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
/** 组件卸载时清除定时器 */
|
||||
onBeforeUnmount(() => {
|
||||
if (autoRefreshTimer) {
|
||||
clearInterval(autoRefreshTimer);
|
||||
autoRefreshTimer = null;
|
||||
}
|
||||
});
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
if (props.deviceId) {
|
||||
getMessageList();
|
||||
}
|
||||
});
|
||||
|
||||
/** 刷新消息列表 */
|
||||
function refresh(delay = 0) {
|
||||
if (delay > 0) {
|
||||
setTimeout(() => {
|
||||
handleQuery();
|
||||
}, delay);
|
||||
} else {
|
||||
handleQuery();
|
||||
}
|
||||
}
|
||||
|
||||
/** 暴露方法给父组件 */
|
||||
defineExpose({
|
||||
refresh,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<!-- 搜索区域 -->
|
||||
<Form :model="queryParams" layout="inline">
|
||||
<Form.Item>
|
||||
<Select
|
||||
v-model:value="queryParams.method"
|
||||
placeholder="所有方法"
|
||||
style="width: 160px"
|
||||
allow-clear
|
||||
>
|
||||
<Select.Option
|
||||
v-for="item in methodOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Select
|
||||
v-model:value="queryParams.upstream"
|
||||
placeholder="上行/下行"
|
||||
style="width: 160px"
|
||||
allow-clear
|
||||
>
|
||||
<Select.Option label="上行" value="true">上行</Select.Option>
|
||||
<Select.Option label="下行" value="false">下行</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" @click="handleQuery">
|
||||
<IconifyIcon icon="ep:search" class="mr-5px" /> 搜索
|
||||
</Button>
|
||||
<Switch
|
||||
v-model:checked="autoRefresh"
|
||||
class="ml-20px"
|
||||
checked-children="定时刷新"
|
||||
un-checked-children="定时刷新"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<!-- 消息列表 -->
|
||||
<Table
|
||||
:loading="loading"
|
||||
:data-source="list"
|
||||
:columns="columns"
|
||||
:pagination="false"
|
||||
class="whitespace-nowrap"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'ts'">
|
||||
{{ formatDate(record.ts) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'upstream'">
|
||||
<Tag :color="record.upstream ? 'blue' : 'green'">
|
||||
{{ record.upstream ? '上行' : '下行' }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'reply'">
|
||||
<DictTag
|
||||
:type="DICT_TYPE.INFRA_BOOLEAN_STRING"
|
||||
:value="record.reply"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'method'">
|
||||
{{
|
||||
methodOptions.find((item) => item.value === record.method)?.label
|
||||
}}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'params'">
|
||||
<span v-if="record.reply">
|
||||
{{
|
||||
`{"code":${record.code},"msg":"${record.msg}","data":${record.data}\}`
|
||||
}}
|
||||
</span>
|
||||
<span v-else>{{ record.params }}</span>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="mt-10px flex justify-end">
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNo"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getMessageList"
|
||||
/>
|
||||
</div>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
@@ -1,605 +0,0 @@
|
||||
<!-- 模拟设备 -->
|
||||
<script lang="ts" setup>
|
||||
import type { TableColumnType } from 'ant-design-vue';
|
||||
|
||||
import type { IotDeviceApi } from '#/api/iot/device/device';
|
||||
import type { IotProductApi } from '#/api/iot/product/product';
|
||||
import type { ThingModelData } from '#/api/iot/thingmodel';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { ContentWrap } from '@vben/common-ui';
|
||||
import { DeviceStateEnum } from '@vben/constants';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Input,
|
||||
message,
|
||||
Table,
|
||||
Tabs,
|
||||
Textarea,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { sendDeviceMessage } from '#/api/iot/device/device';
|
||||
import {
|
||||
IotDeviceMessageMethodEnum,
|
||||
IoTThingModelTypeEnum,
|
||||
} from '#/views/iot/utils/constants';
|
||||
|
||||
import DataDefinition from '../../../../../thingmodel/modules/components/data-definition.vue';
|
||||
import DeviceDetailsMessage from './message.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
device: IotDeviceApi.Device;
|
||||
product: IotProductApi.Product;
|
||||
thingModelList: ThingModelData[];
|
||||
}>();
|
||||
|
||||
// 消息弹窗
|
||||
const activeTab = ref('upstream'); // 上行upstream、下行downstream
|
||||
const upstreamTab = ref(IotDeviceMessageMethodEnum.PROPERTY_POST.method); // 上行子标签
|
||||
const downstreamTab = ref(IotDeviceMessageMethodEnum.PROPERTY_SET.method); // 下行子标签
|
||||
const deviceMessageRef = ref(); // 设备消息组件引用
|
||||
const deviceMessageRefreshDelay = 2000; // 延迟 N 秒,保证模拟上行的消息被处理
|
||||
|
||||
// 折叠状态
|
||||
const debugCollapsed = ref(false); // 指令调试区域折叠状态
|
||||
const messageCollapsed = ref(false); // 设备消息区域折叠状态
|
||||
|
||||
// 表单数据:存储用户输入的模拟值
|
||||
const formData = ref<Record<string, string>>({});
|
||||
|
||||
// 根据类型过滤物模型数据
|
||||
const getFilteredThingModelList = (type: number) => {
|
||||
return props.thingModelList.filter(
|
||||
(item) => String(item.type) === String(type),
|
||||
);
|
||||
};
|
||||
|
||||
// 计算属性:属性列表
|
||||
const propertyList = computed(() =>
|
||||
getFilteredThingModelList(IoTThingModelTypeEnum.PROPERTY),
|
||||
);
|
||||
|
||||
// 计算属性:事件列表
|
||||
const eventList = computed(() =>
|
||||
getFilteredThingModelList(IoTThingModelTypeEnum.EVENT),
|
||||
);
|
||||
|
||||
// 计算属性:服务列表
|
||||
const serviceList = computed(() =>
|
||||
getFilteredThingModelList(IoTThingModelTypeEnum.SERVICE),
|
||||
);
|
||||
|
||||
// 属性表格列定义
|
||||
const propertyColumns: TableColumnType[] = [
|
||||
{
|
||||
title: '功能名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 120,
|
||||
fixed: 'left' as any,
|
||||
},
|
||||
{
|
||||
title: '标识符',
|
||||
dataIndex: 'identifier',
|
||||
key: 'identifier',
|
||||
width: 120,
|
||||
fixed: 'left' as any,
|
||||
},
|
||||
{
|
||||
title: '数据类型',
|
||||
key: 'dataType',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '数据定义',
|
||||
key: 'dataDefinition',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
title: '值',
|
||||
key: 'value',
|
||||
width: 300,
|
||||
fixed: 'right' as any,
|
||||
},
|
||||
];
|
||||
|
||||
// 事件表格列定义
|
||||
const eventColumns = [
|
||||
{
|
||||
title: '功能名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 120,
|
||||
fixed: 'left' as any,
|
||||
},
|
||||
{
|
||||
title: '标识符',
|
||||
dataIndex: 'identifier',
|
||||
key: 'identifier',
|
||||
width: 120,
|
||||
fixed: 'left' as any,
|
||||
},
|
||||
{
|
||||
title: '数据类型',
|
||||
key: 'dataType',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '数据定义',
|
||||
key: 'dataDefinition',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
title: '值',
|
||||
key: 'value',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 100,
|
||||
fixed: 'right' as any,
|
||||
},
|
||||
];
|
||||
|
||||
// 服务表格列定义
|
||||
const serviceColumns = [
|
||||
{
|
||||
title: '服务名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 120,
|
||||
fixed: 'left' as any,
|
||||
},
|
||||
{
|
||||
title: '标识符',
|
||||
dataIndex: 'identifier',
|
||||
key: 'identifier',
|
||||
width: 120,
|
||||
fixed: 'left' as any,
|
||||
},
|
||||
{
|
||||
title: '输入参数',
|
||||
key: 'dataDefinition',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
title: '参数值',
|
||||
key: 'value',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 100,
|
||||
fixed: 'right' as any,
|
||||
},
|
||||
];
|
||||
|
||||
// 获取表单值
|
||||
function getFormValue(identifier: string) {
|
||||
return formData.value[identifier] || '';
|
||||
}
|
||||
|
||||
// 设置表单值
|
||||
function setFormValue(identifier: string, value: string) {
|
||||
formData.value[identifier] = value;
|
||||
}
|
||||
|
||||
// 属性上报
|
||||
async function handlePropertyPost() {
|
||||
try {
|
||||
const params: Record<string, any> = {};
|
||||
propertyList.value.forEach((item) => {
|
||||
const value = formData.value[item.identifier!];
|
||||
if (value) {
|
||||
params[item.identifier!] = value;
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(params).length === 0) {
|
||||
message.warning({ content: '请至少输入一个属性值' });
|
||||
return;
|
||||
}
|
||||
|
||||
await sendDeviceMessage({
|
||||
deviceId: props.device.id!,
|
||||
method: IotDeviceMessageMethodEnum.PROPERTY_POST.method,
|
||||
params,
|
||||
});
|
||||
|
||||
message.success({ content: '属性上报成功' });
|
||||
// 延迟刷新设备消息列表
|
||||
deviceMessageRef.value?.refresh(deviceMessageRefreshDelay);
|
||||
} catch (error) {
|
||||
message.error({ content: '属性上报失败' });
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
// 事件上报
|
||||
async function handleEventPost(row: ThingModelData) {
|
||||
try {
|
||||
const valueStr = formData.value[row.identifier!];
|
||||
let params: any = {};
|
||||
|
||||
if (valueStr) {
|
||||
try {
|
||||
params = JSON.parse(valueStr);
|
||||
} catch {
|
||||
message.error({ content: '事件参数格式错误,请输入有效的JSON格式' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await sendDeviceMessage({
|
||||
deviceId: props.device.id!,
|
||||
method: IotDeviceMessageMethodEnum.EVENT_POST.method,
|
||||
params: {
|
||||
identifier: row.identifier,
|
||||
params,
|
||||
},
|
||||
});
|
||||
|
||||
message.success({ content: '事件上报成功' });
|
||||
// 延迟刷新设备消息列表
|
||||
deviceMessageRef.value?.refresh(deviceMessageRefreshDelay);
|
||||
} catch (error) {
|
||||
message.error({ content: '事件上报失败' });
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
// 状态变更
|
||||
async function handleDeviceState(state: number) {
|
||||
try {
|
||||
await sendDeviceMessage({
|
||||
deviceId: props.device.id!,
|
||||
method: IotDeviceMessageMethodEnum.STATE_UPDATE.method,
|
||||
params: { state },
|
||||
});
|
||||
|
||||
message.success({ content: '状态变更成功' });
|
||||
// 延迟刷新设备消息列表
|
||||
deviceMessageRef.value?.refresh(deviceMessageRefreshDelay);
|
||||
} catch (error) {
|
||||
message.error({ content: '状态变更失败' });
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
// 属性设置
|
||||
async function handlePropertySet() {
|
||||
try {
|
||||
const params: Record<string, any> = {};
|
||||
propertyList.value.forEach((item) => {
|
||||
const value = formData.value[item.identifier!];
|
||||
if (value) {
|
||||
params[item.identifier!] = value;
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(params).length === 0) {
|
||||
message.warning({ content: '请至少输入一个属性值' });
|
||||
return;
|
||||
}
|
||||
|
||||
await sendDeviceMessage({
|
||||
deviceId: props.device.id!,
|
||||
method: IotDeviceMessageMethodEnum.PROPERTY_SET.method,
|
||||
params,
|
||||
});
|
||||
|
||||
message.success({ content: '属性设置成功' });
|
||||
// 延迟刷新设备消息列表
|
||||
deviceMessageRef.value?.refresh(deviceMessageRefreshDelay);
|
||||
} catch (error) {
|
||||
message.error({ content: '属性设置失败' });
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
// 服务调用
|
||||
async function handleServiceInvoke(row: ThingModelData) {
|
||||
try {
|
||||
const valueStr = formData.value[row.identifier!];
|
||||
let params: any = {};
|
||||
|
||||
if (valueStr) {
|
||||
try {
|
||||
params = JSON.parse(valueStr);
|
||||
} catch {
|
||||
message.error({ content: '服务参数格式错误,请输入有效的JSON格式' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await sendDeviceMessage({
|
||||
deviceId: props.device.id!,
|
||||
method: IotDeviceMessageMethodEnum.SERVICE_INVOKE.method,
|
||||
params: {
|
||||
identifier: row.identifier,
|
||||
params,
|
||||
},
|
||||
});
|
||||
|
||||
message.success({ content: '服务调用成功' });
|
||||
// 延迟刷新设备消息列表
|
||||
deviceMessageRef.value?.refresh(deviceMessageRefreshDelay);
|
||||
} catch (error) {
|
||||
message.error({ content: '服务调用失败' });
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<!-- 上方:指令调试区域 -->
|
||||
<!-- TODO @haohao:要不要改成左右; -->
|
||||
<Card class="simulator-tabs mb-4">
|
||||
<template #title>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>指令调试</span>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
@click="debugCollapsed = !debugCollapsed"
|
||||
>
|
||||
<IconifyIcon icon="lucide:chevron-up" v-if="!debugCollapsed" />
|
||||
<IconifyIcon icon="lucide:chevron-down" v-if="debugCollapsed" />
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
<div v-show="!debugCollapsed">
|
||||
<Tabs v-model:active-key="activeTab" size="small">
|
||||
<!-- 上行指令调试 -->
|
||||
<Tabs.TabPane key="upstream" tab="上行指令调试">
|
||||
<Tabs
|
||||
v-if="activeTab === 'upstream'"
|
||||
v-model:active-key="upstreamTab"
|
||||
size="small"
|
||||
>
|
||||
<!-- 属性上报 -->
|
||||
<Tabs.TabPane
|
||||
:key="IotDeviceMessageMethodEnum.PROPERTY_POST.method"
|
||||
tab="属性上报"
|
||||
>
|
||||
<ContentWrap>
|
||||
<Table
|
||||
:data-source="propertyList"
|
||||
align="center"
|
||||
:columns="propertyColumns"
|
||||
:pagination="false"
|
||||
:scroll="{ y: 300 }"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'dataType'">
|
||||
{{ record.property?.dataType ?? '-' }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'dataDefinition'">
|
||||
<DataDefinition :data="record" />
|
||||
</template>
|
||||
<template v-else-if="column.key === 'value'">
|
||||
<Input
|
||||
:value="getFormValue(record.identifier)"
|
||||
@update:value="
|
||||
setFormValue(record.identifier, $event)
|
||||
"
|
||||
placeholder="输入值"
|
||||
size="small"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600">
|
||||
设置属性值后,点击「发送属性上报」按钮
|
||||
</span>
|
||||
<Button type="primary" @click="handlePropertyPost">
|
||||
发送属性上报
|
||||
</Button>
|
||||
</div>
|
||||
</ContentWrap>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<!-- 事件上报 -->
|
||||
<Tabs.TabPane
|
||||
:key="IotDeviceMessageMethodEnum.EVENT_POST.method"
|
||||
tab="事件上报"
|
||||
>
|
||||
<ContentWrap>
|
||||
<Table
|
||||
:data-source="eventList"
|
||||
align="center"
|
||||
:columns="eventColumns"
|
||||
:pagination="false"
|
||||
:scroll="{ y: 300 }"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'dataType'">
|
||||
{{ record.event?.dataType ?? '-' }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'dataDefinition'">
|
||||
<DataDefinition :data="record" />
|
||||
</template>
|
||||
<template v-else-if="column.key === 'value'">
|
||||
<Textarea
|
||||
:value="getFormValue(record.identifier)"
|
||||
@update:value="
|
||||
setFormValue(record.identifier, $event)
|
||||
"
|
||||
:rows="3"
|
||||
placeholder="输入事件参数(JSON格式)"
|
||||
size="small"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleEventPost(record)"
|
||||
>
|
||||
上报事件
|
||||
</Button>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
</ContentWrap>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<!-- 状态变更 -->
|
||||
<Tabs.TabPane
|
||||
:key="IotDeviceMessageMethodEnum.STATE_UPDATE.method"
|
||||
tab="状态变更"
|
||||
>
|
||||
<ContentWrap>
|
||||
<div class="flex gap-4">
|
||||
<Button
|
||||
type="primary"
|
||||
@click="handleDeviceState(DeviceStateEnum.ONLINE)"
|
||||
>
|
||||
设备上线
|
||||
</Button>
|
||||
<Button
|
||||
danger
|
||||
@click="handleDeviceState(DeviceStateEnum.OFFLINE)"
|
||||
>
|
||||
设备下线
|
||||
</Button>
|
||||
</div>
|
||||
</ContentWrap>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<!-- 下行指令调试 -->
|
||||
<Tabs.TabPane key="downstream" tab="下行指令调试">
|
||||
<Tabs
|
||||
v-if="activeTab === 'downstream'"
|
||||
v-model:active-key="downstreamTab"
|
||||
size="small"
|
||||
>
|
||||
<!-- 属性调试 -->
|
||||
<Tabs.TabPane
|
||||
:key="IotDeviceMessageMethodEnum.PROPERTY_SET.method"
|
||||
tab="属性设置"
|
||||
>
|
||||
<ContentWrap>
|
||||
<Table
|
||||
:data-source="propertyList"
|
||||
align="center"
|
||||
:columns="propertyColumns"
|
||||
:pagination="false"
|
||||
:scroll="{ y: 300 }"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'dataType'">
|
||||
{{ record.property?.dataType ?? '-' }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'dataDefinition'">
|
||||
<DataDefinition :data="record" />
|
||||
</template>
|
||||
<template v-else-if="column.key === 'value'">
|
||||
<Input
|
||||
:value="getFormValue(record.identifier)"
|
||||
@update:value="
|
||||
setFormValue(record.identifier, $event)
|
||||
"
|
||||
placeholder="输入值"
|
||||
size="small"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600">
|
||||
设置属性值后,点击「发送属性设置」按钮
|
||||
</span>
|
||||
<Button type="primary" @click="handlePropertySet">
|
||||
发送属性设置
|
||||
</Button>
|
||||
</div>
|
||||
</ContentWrap>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<!-- 服务调用 -->
|
||||
<Tabs.TabPane
|
||||
:key="IotDeviceMessageMethodEnum.SERVICE_INVOKE.method"
|
||||
tab="设备服务调用"
|
||||
>
|
||||
<ContentWrap>
|
||||
<Table
|
||||
:data-source="serviceList"
|
||||
align="center"
|
||||
:columns="serviceColumns"
|
||||
:pagination="false"
|
||||
:scroll="{ y: 300 }"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'dataDefinition'">
|
||||
<DataDefinition :data="record" />
|
||||
</template>
|
||||
<template v-else-if="column.key === 'value'">
|
||||
<Textarea
|
||||
:value="getFormValue(record.identifier)"
|
||||
@update:value="
|
||||
setFormValue(record.identifier, $event)
|
||||
"
|
||||
:rows="3"
|
||||
placeholder="输入服务参数(JSON格式)"
|
||||
size="small"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleServiceInvoke(record)"
|
||||
>
|
||||
服务调用
|
||||
</Button>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
</ContentWrap>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<!-- 下方:设备消息区域 -->
|
||||
<Card>
|
||||
<template #title>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>设备消息</span>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
@click="messageCollapsed = !messageCollapsed"
|
||||
>
|
||||
<IconifyIcon icon="lucide:chevron-down" v-if="!messageCollapsed" />
|
||||
<IconifyIcon icon="lucide:chevron-down" v-if="messageCollapsed" />
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
<div v-show="!messageCollapsed">
|
||||
<DeviceDetailsMessage
|
||||
v-if="device.id"
|
||||
ref="deviceMessageRef"
|
||||
:device-id="device.id"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
@@ -1,237 +0,0 @@
|
||||
<!-- 设备事件管理 -->
|
||||
<script lang="ts" setup>
|
||||
// TODO @haohao:看看能不能用 Grid 实现下,方便 element-plus 迁移
|
||||
import type { ThingModelData } from '#/api/iot/thingmodel';
|
||||
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import { ContentWrap } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { formatDate } from '@vben/utils';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
Form,
|
||||
Pagination,
|
||||
RangePicker,
|
||||
Select,
|
||||
Table,
|
||||
Tag,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { getDeviceMessagePairPage } from '#/api/iot/device/device';
|
||||
import {
|
||||
getEventTypeLabel,
|
||||
IotDeviceMessageMethodEnum,
|
||||
IoTThingModelTypeEnum,
|
||||
} from '#/views/iot/utils/constants';
|
||||
|
||||
const props = defineProps<{
|
||||
deviceId: number;
|
||||
thingModelList: ThingModelData[];
|
||||
}>();
|
||||
|
||||
const loading = ref(false); // 列表的加载中
|
||||
const total = ref(0); // 列表的总页数
|
||||
const list = ref([] as any[]); // 列表的数据
|
||||
const queryParams = reactive({
|
||||
deviceId: props.deviceId,
|
||||
method: IotDeviceMessageMethodEnum.EVENT_POST.method, // 固定筛选事件消息
|
||||
identifier: '',
|
||||
times: undefined,
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
});
|
||||
const queryFormRef = ref(); // 搜索的表单
|
||||
|
||||
/** 事件类型的物模型数据 */
|
||||
const eventThingModels = computed(() => {
|
||||
return props.thingModelList.filter(
|
||||
(item: ThingModelData) =>
|
||||
String(item.type) === String(IoTThingModelTypeEnum.EVENT),
|
||||
);
|
||||
});
|
||||
|
||||
/** 查询列表 */
|
||||
async function getList() {
|
||||
if (!props.deviceId) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await getDeviceMessagePairPage(queryParams);
|
||||
list.value = data.list || [];
|
||||
total.value = data.total || 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery() {
|
||||
queryParams.pageNo = 1;
|
||||
getList();
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function resetQuery() {
|
||||
queryFormRef.value?.resetFields();
|
||||
queryParams.identifier = '';
|
||||
queryParams.times = undefined;
|
||||
handleQuery();
|
||||
}
|
||||
|
||||
/** 获取事件名称 */
|
||||
function getEventName(identifier: string | undefined) {
|
||||
if (!identifier) return '-';
|
||||
const event = eventThingModels.value.find(
|
||||
(item: ThingModelData) => item.identifier === identifier,
|
||||
);
|
||||
return event?.name || identifier;
|
||||
}
|
||||
|
||||
/** 获取事件类型 */
|
||||
function getEventType(identifier: string | undefined) {
|
||||
if (!identifier) return '-';
|
||||
const event = eventThingModels.value.find(
|
||||
(item: ThingModelData) => item.identifier === identifier,
|
||||
);
|
||||
if (!event?.event?.type) return '-';
|
||||
return getEventTypeLabel(event.event.type) || '-';
|
||||
}
|
||||
|
||||
/** 解析参数 */
|
||||
function parseParams(params: string) {
|
||||
try {
|
||||
const parsed = JSON.parse(params);
|
||||
if (parsed.params) {
|
||||
return parsed.params;
|
||||
}
|
||||
return parsed;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<Form
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
layout="inline"
|
||||
@submit.prevent
|
||||
style="margin-bottom: 16px"
|
||||
>
|
||||
<Form.Item label="标识符" name="identifier">
|
||||
<Select
|
||||
v-model:value="queryParams.identifier"
|
||||
placeholder="请选择事件标识符"
|
||||
allow-clear
|
||||
style="width: 240px"
|
||||
>
|
||||
<Select.Option
|
||||
v-for="event in eventThingModels"
|
||||
:key="event.identifier"
|
||||
:value="event.identifier!"
|
||||
>
|
||||
{{ event.name }}({{ event.identifier }})
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="时间范围" name="times">
|
||||
<RangePicker
|
||||
v-model:value="queryParams.times"
|
||||
show-time
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
style="width: 360px"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" @click="handleQuery">
|
||||
<template #icon>
|
||||
<IconifyIcon icon="ep:search" />
|
||||
</template>
|
||||
搜索
|
||||
</Button>
|
||||
<Button @click="resetQuery" style="margin-left: 8px">
|
||||
<template #icon>
|
||||
<IconifyIcon icon="ep:refresh" />
|
||||
</template>
|
||||
重置
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<Divider style="margin: 16px 0" />
|
||||
|
||||
<!-- 事件列表 -->
|
||||
<Table v-loading="loading" :data-source="list" :pagination="false">
|
||||
<Table.Column
|
||||
title="上报时间"
|
||||
align="center"
|
||||
data-index="reportTime"
|
||||
:width="180"
|
||||
>
|
||||
<template #default="{ record }">
|
||||
{{
|
||||
record.request?.reportTime
|
||||
? formatDate(record.request.reportTime)
|
||||
: '-'
|
||||
}}
|
||||
</template>
|
||||
</Table.Column>
|
||||
<Table.Column
|
||||
title="标识符"
|
||||
align="center"
|
||||
data-index="identifier"
|
||||
:width="160"
|
||||
>
|
||||
<template #default="{ record }">
|
||||
<Tag color="blue" size="small">
|
||||
{{ record.request?.identifier }}
|
||||
</Tag>
|
||||
</template>
|
||||
</Table.Column>
|
||||
<Table.Column
|
||||
title="事件名称"
|
||||
align="center"
|
||||
data-index="eventName"
|
||||
:width="160"
|
||||
>
|
||||
<template #default="{ record }">
|
||||
{{ getEventName(record.request?.identifier) }}
|
||||
</template>
|
||||
</Table.Column>
|
||||
<Table.Column
|
||||
title="事件类型"
|
||||
align="center"
|
||||
data-index="eventType"
|
||||
:width="100"
|
||||
>
|
||||
<template #default="{ record }">
|
||||
{{ getEventType(record.request?.identifier) }}
|
||||
</template>
|
||||
</Table.Column>
|
||||
<Table.Column title="输入参数" align="center" data-index="params">
|
||||
<template #default="{ record }">
|
||||
{{ parseParams(record.request.params) }}
|
||||
</template>
|
||||
</Table.Column>
|
||||
</Table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNo"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
@@ -1,259 +0,0 @@
|
||||
<!-- 设备服务调用 -->
|
||||
<script lang="ts" setup>
|
||||
// TODO @haohao:看看能不能调整成 Grid 风格~方便 element-plus 的迁移
|
||||
import type { ThingModelData } from '#/api/iot/thingmodel';
|
||||
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import { ContentWrap } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { formatDate } from '@vben/utils';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
Form,
|
||||
Pagination,
|
||||
Select,
|
||||
Table,
|
||||
Tag,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { getDeviceMessagePairPage } from '#/api/iot/device/device';
|
||||
import {
|
||||
getThingModelServiceCallTypeLabel,
|
||||
IotDeviceMessageMethodEnum,
|
||||
IoTThingModelTypeEnum,
|
||||
} from '#/views/iot/utils/constants';
|
||||
|
||||
const props = defineProps<{
|
||||
deviceId: number;
|
||||
thingModelList: ThingModelData[];
|
||||
}>();
|
||||
|
||||
const loading = ref(false); // 列表的加载中
|
||||
const total = ref(0); // 列表的总页数
|
||||
const list = ref([] as any[]); // 列表的数据
|
||||
const queryParams = reactive({
|
||||
deviceId: props.deviceId,
|
||||
method: IotDeviceMessageMethodEnum.SERVICE_INVOKE.method, // 固定筛选服务调用消息
|
||||
identifier: '',
|
||||
times: [] as any[],
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
});
|
||||
const queryFormRef = ref(); // 搜索的表单
|
||||
|
||||
/** 服务类型的物模型数据 */
|
||||
const serviceThingModels = computed(() => {
|
||||
return props.thingModelList.filter(
|
||||
(item: ThingModelData) =>
|
||||
String(item.type) === String(IoTThingModelTypeEnum.SERVICE),
|
||||
);
|
||||
});
|
||||
|
||||
/** 查询列表 */
|
||||
async function getList() {
|
||||
if (!props.deviceId) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await getDeviceMessagePairPage(queryParams);
|
||||
list.value = data.list || [];
|
||||
total.value = data.total;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery() {
|
||||
queryParams.pageNo = 1;
|
||||
getList();
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function resetQuery() {
|
||||
queryFormRef.value?.resetFields();
|
||||
queryParams.identifier = '';
|
||||
queryParams.times = [];
|
||||
handleQuery();
|
||||
}
|
||||
|
||||
/** 获取服务名称 */
|
||||
function getServiceName(identifier: string | undefined) {
|
||||
if (!identifier) return '-';
|
||||
const service = serviceThingModels.value.find(
|
||||
(item: ThingModelData) => item.identifier === identifier,
|
||||
);
|
||||
return service?.name || identifier;
|
||||
}
|
||||
|
||||
/** 获取调用方式 */
|
||||
function getCallType(identifier: string | undefined) {
|
||||
if (!identifier) return '-';
|
||||
const service = serviceThingModels.value.find(
|
||||
(item: ThingModelData) => item.identifier === identifier,
|
||||
);
|
||||
if (!service?.service?.callType) return '-';
|
||||
return getThingModelServiceCallTypeLabel(service.service.callType) || '-';
|
||||
}
|
||||
|
||||
/** 解析参数 */
|
||||
function parseParams(params: string) {
|
||||
if (!params) return '-';
|
||||
try {
|
||||
const parsed = JSON.parse(params);
|
||||
if (parsed.params) {
|
||||
return JSON.stringify(parsed.params, null, 2);
|
||||
}
|
||||
return JSON.stringify(parsed, null, 2);
|
||||
} catch {
|
||||
return params;
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<Form
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
layout="inline"
|
||||
@submit.prevent
|
||||
style="margin-bottom: 16px"
|
||||
>
|
||||
<Form.Item label="标识符" name="identifier">
|
||||
<Select
|
||||
v-model:value="queryParams.identifier"
|
||||
placeholder="请选择服务标识符"
|
||||
allow-clear
|
||||
style="width: 240px"
|
||||
>
|
||||
<Select.Option
|
||||
v-for="service in serviceThingModels"
|
||||
:key="service.identifier"
|
||||
:value="service.identifier!"
|
||||
>
|
||||
{{ service.name }}({{ service.identifier }})
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="时间范围" name="times">
|
||||
<RangePicker
|
||||
v-model:value="queryParams.times"
|
||||
show-time
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
style="width: 360px"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" @click="handleQuery">
|
||||
<template #icon>
|
||||
<IconifyIcon icon="ep:search" />
|
||||
</template>
|
||||
搜索
|
||||
</Button>
|
||||
<Button @click="resetQuery" style="margin-left: 8px">
|
||||
<template #icon>
|
||||
<IconifyIcon icon="ep:refresh" />
|
||||
</template>
|
||||
重置
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<Divider style="margin: 16px 0" />
|
||||
|
||||
<!-- 服务调用列表 -->
|
||||
<Table v-loading="loading" :data-source="list" :pagination="false">
|
||||
<Table.Column
|
||||
title="调用时间"
|
||||
align="center"
|
||||
data-index="requestTime"
|
||||
:width="180"
|
||||
>
|
||||
<template #default="{ record }">
|
||||
{{
|
||||
record.request?.reportTime
|
||||
? formatDate(record.request.reportTime)
|
||||
: '-'
|
||||
}}
|
||||
</template>
|
||||
</Table.Column>
|
||||
<Table.Column
|
||||
title="响应时间"
|
||||
align="center"
|
||||
data-index="responseTime"
|
||||
:width="180"
|
||||
>
|
||||
<template #default="{ record }">
|
||||
{{
|
||||
record.reply?.reportTime ? formatDate(record.reply.reportTime) : '-'
|
||||
}}
|
||||
</template>
|
||||
</Table.Column>
|
||||
<Table.Column
|
||||
title="标识符"
|
||||
align="center"
|
||||
data-index="identifier"
|
||||
:width="160"
|
||||
>
|
||||
<template #default="{ record }">
|
||||
<Tag color="blue" size="small">
|
||||
{{ record.request?.identifier }}
|
||||
</Tag>
|
||||
</template>
|
||||
</Table.Column>
|
||||
<Table.Column
|
||||
title="服务名称"
|
||||
align="center"
|
||||
data-index="serviceName"
|
||||
:width="160"
|
||||
>
|
||||
<template #default="{ record }">
|
||||
{{ getServiceName(record.request?.identifier) }}
|
||||
</template>
|
||||
</Table.Column>
|
||||
<Table.Column
|
||||
title="调用方式"
|
||||
align="center"
|
||||
data-index="callType"
|
||||
:width="100"
|
||||
>
|
||||
<template #default="{ record }">
|
||||
{{ getCallType(record.request?.identifier) }}
|
||||
</template>
|
||||
</Table.Column>
|
||||
<Table.Column title="输入参数" align="center" data-index="inputParams">
|
||||
<template #default="{ record }">
|
||||
{{ parseParams(record.request?.params) }}
|
||||
</template>
|
||||
</Table.Column>
|
||||
<Table.Column title="输出参数" align="center" data-index="outputParams">
|
||||
<template #default="{ record }">
|
||||
<span v-if="record.reply">
|
||||
{{
|
||||
`{"code":${record.reply.code},"msg":"${record.reply.msg}","data":${record.reply.data}\}`
|
||||
}}
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</Table.Column>
|
||||
</Table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNo"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
@@ -29,7 +29,7 @@ const [Form, formApi] = useVbenForm({
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
wrapperClass: 'grid-cols-2',
|
||||
wrapperClass: 'grid-cols-1',
|
||||
layout: 'horizontal',
|
||||
schema: useFormSchema(),
|
||||
showDefaultActions: false,
|
||||
|
||||
@@ -14,18 +14,13 @@ import { getSimpleProductCategoryList } from '#/api/iot/product/category';
|
||||
|
||||
/** 产品分类列表缓存 */
|
||||
let categoryList: IotProductCategoryApi.ProductCategory[] = [];
|
||||
|
||||
/** 加载产品分类数据 */
|
||||
async function loadCategoryData() {
|
||||
categoryList = await getSimpleProductCategoryList();
|
||||
}
|
||||
|
||||
// 初始化加载分类数据
|
||||
// TODO @haohao:可以参考 /Users/yunai/Java/yudao-ui-admin-vben-v5/apps/web-antd/src/views/system/tenant/data.ts 简洁一点。
|
||||
loadCategoryData();
|
||||
getSimpleProductCategoryList().then((data) => (categoryList = data));
|
||||
|
||||
/** 新增/修改产品的表单 */
|
||||
export function useFormSchema(formApi?: any): VbenFormSchema[] {
|
||||
export function useFormSchema(
|
||||
formApi?: any,
|
||||
generateProductKey?: () => string,
|
||||
): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
@@ -59,7 +54,9 @@ export function useFormSchema(formApi?: any): VbenFormSchema[] {
|
||||
{
|
||||
type: 'default',
|
||||
onClick: () => {
|
||||
formApi?.setFieldValue('productKey', generateProductKey());
|
||||
if (generateProductKey) {
|
||||
formApi?.setFieldValue('productKey', generateProductKey());
|
||||
}
|
||||
},
|
||||
},
|
||||
{ default: () => '重新生成' },
|
||||
@@ -175,7 +172,10 @@ export function useFormSchema(formApi?: any): VbenFormSchema[] {
|
||||
}
|
||||
|
||||
/** 基础表单字段(不含图标、图片、描述) */
|
||||
export function useBasicFormSchema(formApi?: any): VbenFormSchema[] {
|
||||
export function useBasicFormSchema(
|
||||
formApi?: any,
|
||||
generateProductKey?: () => string,
|
||||
): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
@@ -208,7 +208,9 @@ export function useBasicFormSchema(formApi?: any): VbenFormSchema[] {
|
||||
{
|
||||
type: 'default',
|
||||
onClick: () => {
|
||||
formApi?.setFieldValue('productKey', generateProductKey());
|
||||
if (generateProductKey) {
|
||||
formApi?.setFieldValue('productKey', generateProductKey());
|
||||
}
|
||||
},
|
||||
},
|
||||
{ default: () => '重新生成' },
|
||||
@@ -299,6 +301,7 @@ export function useBasicFormSchema(formApi?: any): VbenFormSchema[] {
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
defaultValue: 0,
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
@@ -339,7 +342,6 @@ export function useAdvancedFormSchema(): VbenFormSchema[] {
|
||||
placeholder: '请输入产品描述',
|
||||
rows: 3,
|
||||
},
|
||||
formItemClass: 'col-span-2', // 让描述占满两列
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -404,13 +406,3 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
];
|
||||
}
|
||||
|
||||
/** 生成 ProductKey(包含大小写字母和数字) */
|
||||
export function generateProductKey(): string {
|
||||
const chars =
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < 16; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ const activeTab = ref('info');
|
||||
provide('product', product); // 提供产品信息给子组件
|
||||
|
||||
/** 获取产品详情 */
|
||||
// TODO @haohao:因为 detail 是独立界面,所以不放在 modules 里,应该放在 web-antd/src/views/iot/product/product/detail/index.vue 里,更合理一些哈。
|
||||
async function getProductData(productId: number) {
|
||||
loading.value = true;
|
||||
try {
|
||||
@@ -91,3 +90,4 @@ onMounted(async () => {
|
||||
</Tabs>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Button, Card, Descriptions, message, Modal } from 'ant-design-vue';
|
||||
|
||||
import { updateProductStatus } from '#/api/iot/product/product';
|
||||
|
||||
import Form from '../../form.vue';
|
||||
import Form from '../../modules/form.vue';
|
||||
|
||||
interface Props {
|
||||
product: IotProductApi.Product;
|
||||
@@ -3,7 +3,7 @@ import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { IotProductCategoryApi } from '#/api/iot/product/category';
|
||||
import type { IotProductApi } from '#/api/iot/product/product';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { nextTick, onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
@@ -51,11 +51,8 @@ async function loadCategories() {
|
||||
function handleSearch() {
|
||||
if (viewMode.value === 'list') {
|
||||
gridApi.formApi.setValues(queryParams.value);
|
||||
gridApi.query();
|
||||
} else {
|
||||
// TODO @haohao:要不 search 也改成 query 方法,更统一一点哈。
|
||||
cardViewRef.value?.search(queryParams.value);
|
||||
}
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 重置搜索 */
|
||||
@@ -67,11 +64,18 @@ function handleReset() {
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
if (viewMode.value === 'list') {
|
||||
gridApi.query();
|
||||
} else {
|
||||
cardViewRef.value?.reload();
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 视图切换 */
|
||||
async function handleViewModeChange(mode: 'card' | 'list') {
|
||||
if (viewMode.value === mode) {
|
||||
return; // 如果已经是目标视图,不需要切换
|
||||
}
|
||||
viewMode.value = mode;
|
||||
// 等待视图更新后再触发查询
|
||||
await nextTick();
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 导出表格 */
|
||||
@@ -149,6 +153,17 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
} as VxeTableGridOptions<IotProductApi.Product>,
|
||||
});
|
||||
|
||||
// 包装 gridApi.query() 方法,统一列表视图和卡片视图的查询接口
|
||||
const originalQuery = gridApi.query.bind(gridApi);
|
||||
gridApi.query = async (params?: Record<string, any>) => {
|
||||
if (viewMode.value === 'list') {
|
||||
return await originalQuery(params);
|
||||
} else {
|
||||
// 卡片视图:调用卡片组件的 query 方法
|
||||
cardViewRef.value?.query();
|
||||
}
|
||||
};
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
loadCategories();
|
||||
@@ -216,13 +231,13 @@ onMounted(() => {
|
||||
<Space :size="4">
|
||||
<Button
|
||||
:type="viewMode === 'card' ? 'primary' : 'default'"
|
||||
@click="viewMode = 'card'"
|
||||
@click="handleViewModeChange('card')"
|
||||
>
|
||||
<IconifyIcon icon="ant-design:appstore-outlined" />
|
||||
</Button>
|
||||
<Button
|
||||
:type="viewMode === 'list' ? 'primary' : 'default'"
|
||||
@click="viewMode = 'list'"
|
||||
@click="handleViewModeChange('list')"
|
||||
>
|
||||
<IconifyIcon icon="ant-design:unordered-list-outlined" />
|
||||
</Button>
|
||||
|
||||
@@ -85,7 +85,7 @@ function getDeviceTypeColor(deviceType: number) {
|
||||
|
||||
defineExpose({
|
||||
reload: getList,
|
||||
search: () => {
|
||||
query: () => {
|
||||
queryParams.value.pageNo = 1;
|
||||
getList();
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import type { IotProductApi } from '#/api/iot/product/product';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
import { computed, nextTick, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
@@ -15,11 +15,18 @@ import {
|
||||
} from '#/api/iot/product/product';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import {
|
||||
generateProductKey,
|
||||
useAdvancedFormSchema,
|
||||
useBasicFormSchema,
|
||||
} from '../data';
|
||||
import { useAdvancedFormSchema, useBasicFormSchema } from '../data';
|
||||
|
||||
/** 生成 ProductKey(包含大小写字母和数字) */
|
||||
function generateProductKey(): string {
|
||||
const chars =
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < 16; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<IotProductApi.Product>();
|
||||
@@ -37,10 +44,9 @@ const [Form, formApi] = useVbenForm({
|
||||
layout: 'horizontal',
|
||||
schema: [],
|
||||
showDefaultActions: false,
|
||||
wrapperClass: 'grid-cols-2',
|
||||
wrapperClass: 'grid-cols-1',
|
||||
});
|
||||
|
||||
// TODO @haohao:这个要不还是一行一个?这样样式好看点哈。
|
||||
const [AdvancedForm, advancedFormApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: { class: 'w-full' },
|
||||
@@ -48,12 +54,24 @@ const [AdvancedForm, advancedFormApi] = useVbenForm({
|
||||
layout: 'horizontal',
|
||||
schema: useAdvancedFormSchema(),
|
||||
showDefaultActions: false,
|
||||
wrapperClass: 'grid-cols-2',
|
||||
wrapperClass: 'grid-cols-1',
|
||||
});
|
||||
|
||||
/** 基础表单需要 formApi 引用,所以通过 setState 设置 schema */
|
||||
// TODO haohao:@haohao:要不要把 generateProductKey 拿到这个 vue 里,作为参数传递到 useBasicFormSchema 里?
|
||||
formApi.setState({ schema: useBasicFormSchema(formApi) });
|
||||
formApi.setState({ schema: useBasicFormSchema(formApi, generateProductKey) });
|
||||
|
||||
/** 获取高级表单的值(如果表单未挂载,则从 formData 中获取) */
|
||||
async function getAdvancedFormValues() {
|
||||
if (advancedFormApi.isMounted) {
|
||||
return await advancedFormApi.getValues();
|
||||
}
|
||||
// 表单未挂载(折叠状态),从 formData 中获取
|
||||
return {
|
||||
icon: formData.value?.icon,
|
||||
picUrl: formData.value?.picUrl,
|
||||
description: formData.value?.description,
|
||||
};
|
||||
}
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
@@ -62,18 +80,9 @@ const [Modal, modalApi] = useVbenModal({
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 合并两个表单的值
|
||||
// 合并两个表单的值(字段不冲突,可直接合并)
|
||||
const basicValues = await formApi.getValues();
|
||||
// TODO @haohao:有 linter 修复下;“formData.value?.id”;另外,这里直接两个表单合并,是不是就可以了呀?因为 2 个 schema 本身不同,字段就不同,不会冲突。
|
||||
const advancedValues = activeKey.value.includes('advanced')
|
||||
? await advancedFormApi.getValues()
|
||||
: formData.value?.id
|
||||
? {
|
||||
icon: formData.value.icon,
|
||||
picUrl: formData.value.picUrl,
|
||||
description: formData.value.description,
|
||||
}
|
||||
: {};
|
||||
const advancedValues = await getAdvancedFormValues();
|
||||
const data = {
|
||||
...basicValues,
|
||||
...advancedValues,
|
||||
@@ -96,12 +105,9 @@ const [Modal, modalApi] = useVbenModal({
|
||||
// 加载数据
|
||||
const data = modalApi.getData<IotProductApi.Product>();
|
||||
if (!data || !data.id) {
|
||||
// 新增:设置默认值
|
||||
// TODO @AI:
|
||||
// 新增:设置默认值(status 通过 schema 中的 defaultValue 自动设置为 0)
|
||||
await formApi.setValues({
|
||||
// TODO @haohao:要不要把 generateProductKey 拿到这个 vue 里,作为参数传递到 useBasicFormSchema 里?
|
||||
productKey: generateProductKey(),
|
||||
status: 0, // TODO @haohao:通过 defaultValue 即可;
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -110,22 +116,10 @@ const [Modal, modalApi] = useVbenModal({
|
||||
try {
|
||||
formData.value = await getProduct(data.id);
|
||||
await formApi.setValues(formData.value);
|
||||
// 设置高级表单(不等待)
|
||||
// TODO @haohao:直接把 formData 传过去?没关系的哈。因为会 filter 掉不存在的值,可以试试哈。
|
||||
// TODO @haohao:这里是不是要 await 下呀?有黄色的告警;
|
||||
advancedFormApi.setValues({
|
||||
icon: formData.value.icon,
|
||||
picUrl: formData.value.picUrl,
|
||||
description: formData.value.description,
|
||||
});
|
||||
// 有高级字段时自动展开
|
||||
// TODO @haohao:默认不用展开哈。
|
||||
if (
|
||||
formData.value.icon ||
|
||||
formData.value.picUrl ||
|
||||
formData.value.description
|
||||
) {
|
||||
activeKey.value = ['advanced'];
|
||||
// 设置高级表单(如果已挂载)
|
||||
await nextTick();
|
||||
if (advancedFormApi.isMounted) {
|
||||
await advancedFormApi.setValues(formData.value);
|
||||
}
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
|
||||
@@ -2,9 +2,13 @@ import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { getSimpleUserList } from '#/api/system/user';
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
@@ -139,6 +143,11 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
field: 'userId',
|
||||
label: '操作人编号',
|
||||
},
|
||||
{
|
||||
field: 'userType',
|
||||
label: '操作人类型',
|
||||
render: (val) => h(DictTag, { type: DICT_TYPE.USER_TYPE, value: val }),
|
||||
},
|
||||
{
|
||||
field: 'userName',
|
||||
label: '操作人名字',
|
||||
|
||||
@@ -52,6 +52,7 @@ async function handleClose(row: MallDiscountActivityApi.DiscountActivity) {
|
||||
} finally {
|
||||
loadingInstance.close();
|
||||
}
|
||||
}
|
||||
|
||||
/** 删除满减活动 */
|
||||
async function handleDelete(row: MallDiscountActivityApi.DiscountActivity) {
|
||||
|
||||
@@ -2,9 +2,13 @@ import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { getSimpleUserList } from '#/api/system/user';
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
@@ -133,12 +137,17 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'traceId',
|
||||
label: '链路追踪',
|
||||
show: (val) => !val,
|
||||
show: (data) => !data?.traceId,
|
||||
},
|
||||
{
|
||||
field: 'userId',
|
||||
label: '操作人编号',
|
||||
},
|
||||
{
|
||||
field: 'userType',
|
||||
label: '操作人类型',
|
||||
render: (val) => h(DictTag, { type: DICT_TYPE.USER_TYPE, value: val }),
|
||||
},
|
||||
{
|
||||
field: 'userName',
|
||||
label: '操作人名字',
|
||||
|
||||
Reference in New Issue
Block a user