2025-10-21 14:13:42 +08:00

812 lines
23 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import type { VbenFormProps } from '#/adapter/form';
import type { VxeGridProps } from '#/adapter/vxe-table';
import { computed, h, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { Page, useVbenModal } from '@vben/common-ui';
import {
Button,
message as Message,
Modal,
Popover,
Tag,
} from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import {
postAggregationDeviceBatchCreateAsync,
postAggregationDeviceCreateAsync,
postAggregationDeviceDeleteAsync,
postAggregationDeviceDeviceCommandForApiAsync,
postAggregationDeviceRepushDeviceInfoToIoTplatform,
postDeviceInfoCacheDeviceDataToRedis,
postDeviceInfoPage,
} from '#/api-client';
import { Icon } from '#/components/icon';
import { Loading } from '#/components/Loading';
import { TableAction } from '#/components/table-action';
import { $t } from '#/locales';
import {
addDeviceFormSchema,
batchAddDeviceFormSchema,
commandFormSchema,
editDeviceFormSchemaEdit,
querySchema,
tableSchema,
} from './schema';
defineOptions({
name: 'DeviceInfo',
});
const router = useRouter();
const formOptions: VbenFormProps = {
schema: querySchema.value,
};
const gridOptions: VxeGridProps<any> = {
checkboxConfig: {
highlight: true,
labelField: 'name',
},
columns: tableSchema.value,
height: 'auto',
keepSource: true,
pagerConfig: {},
toolbarConfig: {
custom: true,
},
customConfig: {
storage: true,
},
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
const { data } = await postDeviceInfoPage({
body: {
pageIndex: page.currentPage,
pageSize: page.pageSize,
...formValues,
},
});
return data;
},
},
},
};
const [Grid, gridApi] = useVbenVxeGrid({ formOptions, gridOptions });
const editRow: Record<string, any> = ref({});
const cacheRefreshLoading = ref(false);
const pageLoading = ref(false);
const loadingTip = ref('缓存刷新中...');
const commandRow: Record<string, any> = ref({});
const [UserModal, userModalApi] = useVbenModal({
draggable: true,
onConfirm: submit,
onBeforeClose: () => {
editRow.value = {};
return true;
},
});
const [CommandModal, commandModalApi] = useVbenModal({
draggable: true,
onConfirm: submitCommand,
onBeforeClose: () => {
commandRow.value = {};
return true;
},
});
const [AddForm, addFormApi] = useVbenForm({
// 默认展开
collapsed: false,
// 所有表单项共用,可单独在表单内覆盖
commonConfig: {
labelWidth: 110,
componentProps: {
class: 'w-4/5',
},
},
layout: 'horizontal',
schema: addDeviceFormSchema.value,
showCollapseButton: false,
showDefaultActions: false,
wrapperClass: 'grid-cols-2',
});
const [EditForm, editFormApi] = useVbenForm({
// 默认展开
collapsed: false,
// 所有表单项共用,可单独在表单内覆盖
commonConfig: {
labelWidth: 110,
componentProps: {
class: 'w-4/5',
},
},
// 提交函数
layout: 'horizontal',
schema: editDeviceFormSchemaEdit.value,
showCollapseButton: false,
showDefaultActions: false,
wrapperClass: 'grid-cols-2',
});
const [CommandForm, commandFormApi] = useVbenForm({
// 默认展开
collapsed: false,
// 所有表单项共用,可单独在表单内覆盖
commonConfig: {
labelWidth: 110,
componentProps: {
class: 'w-full',
},
},
layout: 'horizontal',
schema: commandFormSchema.value,
showCollapseButton: false,
showDefaultActions: false,
});
const [BatchAddForm, batchAddFormApi] = useVbenForm({
// 默认展开
collapsed: false,
// 所有表单项共用,可单独在表单内覆盖
commonConfig: {
labelWidth: 110,
componentProps: {
class: 'w-4/5',
},
},
layout: 'horizontal',
schema: batchAddDeviceFormSchema.value,
showCollapseButton: false,
showDefaultActions: false,
wrapperClass: 'grid-cols-2',
// 添加响应式监听
autoSubmitOnEnter: false,
});
const [BatchAddModal, batchAddModalApi] = useVbenModal({
draggable: true,
onConfirm: submitBatchAdd,
onBeforeClose: () => {
batchAddFormApi.resetForm();
stopLineCheck();
return true;
},
onOpenChange: (isOpen) => {
if (isOpen) {
// 模态框打开时启动行数检查
setTimeout(() => {
startLineCheck();
}, 100);
} else {
// 模态框关闭时停止行数检查
stopLineCheck();
}
},
});
// 获取批量添加模态框的状态
const batchAddModalState = batchAddModalApi.useStore();
// 新增和编辑提交的逻辑
async function submit() {
const isEdit = !!editRow.value.id;
const formApi = isEdit ? editFormApi : addFormApi;
const api = postAggregationDeviceCreateAsync; // 目前只有创建接口,编辑也使用创建接口
const { valid } = await formApi.validate();
if (!valid) return;
const formValues = await formApi.getValues();
// 根据平台类型处理数据
const processedFormValues = { ...formValues };
if (formValues.ioTPlatform === 2 || formValues.ioTPlatform === '2') {
// OneNET平台
processedFormValues.ioTPlatformAccountId = formValues.oneNETAccountId;
processedFormValues.ioTPlatformProductId = formValues.oneNETProductId;
// 清理不需要的字段
delete processedFormValues.oneNETAccountId;
delete processedFormValues.oneNETProductId;
delete processedFormValues.ctWingAccountId;
delete processedFormValues.ctWingProductId;
} else if (formValues.ioTPlatform === 1 || formValues.ioTPlatform === '1') {
// CTWing平台
processedFormValues.ioTPlatformAccountId = formValues.ctWingAccountId;
processedFormValues.ioTPlatformProductId = formValues.ctWingProductId;
// 清理不需要的字段
delete processedFormValues.ctWingAccountId;
delete processedFormValues.ctWingProductId;
delete processedFormValues.oneNETAccountId;
delete processedFormValues.oneNETProductId;
}
const fetchParams: any = isEdit
? {
id: editRow.value.id,
...processedFormValues,
}
: {
...processedFormValues,
};
try {
userModalApi.setState({ loading: true, confirmLoading: true });
const resp = await api({ body: fetchParams });
if (resp.data) {
Message.success(
editRow.value.id ? $t('common.editSuccess') : $t('common.addSuccess'),
);
userModalApi.close();
editRow.value = {};
gridApi.reload();
} else {
Message.error(
editRow.value.id ? $t('common.editFail') : $t('common.addFail'),
);
}
} catch (error) {
console.error('设备操作失败:', error);
Message.error(
editRow.value.id ? $t('common.editFail') : $t('common.addFail'),
);
} finally {
userModalApi.setState({ loading: false, confirmLoading: false });
}
}
async function onEdit(record: any) {
editRow.value = record;
userModalApi.open();
// 根据平台类型设置表单值
const formValues = { ...record };
// 确保ioTPlatform是字符串格式因为ApiSelect组件的valueField是'key'
if (formValues.ioTPlatform !== undefined && formValues.ioTPlatform !== null) {
formValues.ioTPlatform = String(formValues.ioTPlatform);
}
if (record.ioTPlatform === 2 || record.ioTPlatform === '2') {
// OneNET平台
formValues.oneNETAccountId = record.ioTPlatformAccountId;
formValues.oneNETProductId = record.ioTPlatformProductId;
} else if (record.ioTPlatform === 1 || record.ioTPlatform === '1') {
// CTWing平台
formValues.ctWingAccountId = record.ioTPlatformAccountId;
formValues.ctWingProductId = record.ioTPlatformProductId;
}
editFormApi.setValues(formValues);
}
function onDel(row: any) {
Modal.confirm({
title: `${$t('common.confirmDelete')}${row.deviceName || row.deviceAddress} ?`,
onOk: async () => {
try {
const result = await postAggregationDeviceDeleteAsync({
body: { id: row.id },
});
if (result.data) {
gridApi.reload();
Message.success($t('common.deleteSuccess'));
} else {
Message.error($t('common.deleteFail'));
}
} catch (error) {
console.error('删除设备失败:', error);
Message.error($t('common.deleteFail'));
}
},
});
}
const toDeviceInfoData = (row: Record<string, any>) => {
// 或者使用编程式导航
router.push({
path: '/iotdbdatamanagement/deviceData',
query: {
DeviceAddress: row.deviceAddress,
IoTDataType: 'Data',
},
});
};
const toDeviceLog = (row: Record<string, any>) => {
// 根据平台类型跳转到对应的日志页面
if (row.ioTPlatform === 1 || row.ioTPlatform === '1') {
// CTWing平台
router.push({
path: '/iotdbdatamanagement/ctwingLog',
query: {
DeviceAddress: row.deviceAddress,
},
});
} else if (row.ioTPlatform === 2 || row.ioTPlatform === '2') {
// OneNET平台
router.push({
path: '/iotdbdatamanagement/onenetLog',
query: {
DeviceAddress: row.deviceAddress,
},
});
}
};
const toTelemetryLog = (row: Record<string, any>) => {
// 跳转到遥测日志页面
router.push({
path: '/iotdbdatamanagement/telemetryLog',
query: {
DeviceAddress: row.deviceAddress,
},
});
};
// 重推设备信息到IoT平台
const repushDeviceInfo = async (row: Record<string, any>) => {
try {
pageLoading.value = true;
loadingTip.value = '重推设备信息中...';
const result = await postAggregationDeviceRepushDeviceInfoToIoTplatform({
body: { id: row.id },
});
if (result.data) {
Message.success('重推设备信息成功');
gridApi.reload();
} else {
Message.error('重推设备信息失败');
}
} catch {
Message.error('重推设备信息失败');
} finally {
pageLoading.value = false;
loadingTip.value = '缓存刷新中...';
}
};
const openAddModal = async () => {
editRow.value = {};
userModalApi.open();
};
const openCommandModal = (row: Record<string, any>) => {
commandRow.value = row;
commandModalApi.open();
};
// 指令提交逻辑
async function submitCommand() {
const { valid } = await commandFormApi.validate();
if (!valid) return;
const formValues = await commandFormApi.getValues();
try {
commandModalApi.setState({ loading: true, confirmLoading: true });
// 调用指令接口
const result = await postAggregationDeviceDeviceCommandForApiAsync({
body: {
Id: commandRow.value.id,
CommandContent: formValues.CommandContent,
},
});
if (result.data) {
Message.success('指令下发成功');
commandModalApi.close();
commandRow.value = {};
} else {
Message.error('指令下发失败');
}
} catch (error) {
console.error('指令发送失败:', error);
Message.error('指令下发失败');
} finally {
commandModalApi.setState({ loading: false, confirmLoading: false });
}
}
// 批量添加设备提交逻辑
async function submitBatchAdd() {
const { valid } = await batchAddFormApi.validate();
if (!valid) return;
const formValues = await batchAddFormApi.getValues();
// 处理设备地址列表
const addressList = formValues.addressList
.split('\n')
.map((address: string) => address.trim())
.filter((address: string) => address.length > 0);
if (addressList.length === 0) {
Message.error('请输入至少一个设备地址');
return;
}
// 验证行数限制
if (addressList.length > 100) {
Message.error('设备地址不能超过100行');
return;
}
// 验证设备地址格式(简单验证)
const invalidAddresses = addressList.filter(
(address: string) => !address || address.length < 3,
);
if (invalidAddresses.length > 0) {
Message.error(`以下设备地址格式不正确: ${invalidAddresses.join(', ')}`);
return;
}
// 根据平台类型处理数据
let ioTPlatformProductId = '';
if (formValues.ioTPlatform === 2 || formValues.ioTPlatform === '2') {
// OneNET平台
ioTPlatformProductId = formValues.oneNETProductId;
} else if (formValues.ioTPlatform === 1 || formValues.ioTPlatform === '1') {
// CTWing平台
ioTPlatformProductId = formValues.ctWingProductId;
}
if (!ioTPlatformProductId) {
Message.error('请选择产品');
return;
}
const batchCreateParams = {
addressList,
ioTPlatform: formValues.ioTPlatform,
ioTPlatformProductId,
};
try {
batchAddModalApi.setState({ loading: true, confirmLoading: true });
const resp = await postAggregationDeviceBatchCreateAsync({
body: batchCreateParams,
});
if (resp.data) {
Message.success(`批量添加设备成功,共添加 ${addressList.length} 个设备`);
batchAddModalApi.close();
batchAddFormApi.resetForm();
gridApi.reload();
} else {
Message.error('批量添加设备失败');
}
} catch (error) {
console.error('批量添加设备失败:', error);
Message.error('批量添加设备失败');
} finally {
batchAddModalApi.setState({ loading: false, confirmLoading: false });
}
}
// 打开批量添加模态框
const openBatchAddModal = () => {
console.log('打开批量添加模态框');
batchAddFormApi.resetForm();
addressLines.value = 0; // 重置行数
batchAddModalApi.open();
};
// 批量添加地址行数
const addressLines = ref(0);
// 检查行数是否超过限制
const isOverLineLimit = computed(() => {
return addressLines.value > 100;
});
// 监听表单值变化,实时更新行数
watch(
() => batchAddFormApi.getValues()?.addressList,
(newValue) => {
try {
console.log('监听地址列表变化:', newValue);
if (!newValue || typeof newValue !== 'string') {
addressLines.value = 0;
return;
}
const lines = newValue.split('\n').filter((line: string) => line.trim());
addressLines.value = lines.length;
console.log('更新行数:', addressLines.value);
} catch {
addressLines.value = 0;
}
},
{ immediate: true },
);
// 监听表单值变化,实时更新行数(备用方案)
watch(
() => batchAddFormApi.getValues(),
(formValues) => {
try {
const addressList = formValues?.addressList;
console.log('监听表单值变化:', addressList);
if (!addressList || typeof addressList !== 'string') {
addressLines.value = 0;
return;
}
const lines = addressList
.split('\n')
.filter((line: string) => line.trim());
addressLines.value = lines.length;
console.log('备用方案更新行数:', addressLines.value);
} catch {
addressLines.value = 0;
}
},
{ deep: true, immediate: true },
);
// 使用定时器定期检查行数(第三种方案)
let lineCheckInterval: NodeJS.Timeout | null = null;
// 在模态框打开时启动定时器
const startLineCheck = () => {
if (lineCheckInterval) {
clearInterval(lineCheckInterval);
}
lineCheckInterval = setInterval(() => {
try {
// 尝试多种方式获取表单值
let addressList = '';
// 方式1直接获取
const formValues = batchAddFormApi.getValues();
console.log('表单值:', formValues);
addressList = formValues?.addressList || '';
// 方式2如果方式1失败尝试其他方法
if (!addressList) {
try {
const rawValues = batchAddFormApi.getValues();
console.log('原始表单值:', rawValues);
addressList = rawValues?.addressList || '';
} catch (error) {
console.log('方式2失败:', error);
}
}
// 方式3直接从DOM获取
if (!addressList) {
const textarea = document.querySelector(
'textarea[name="addressList"]',
) as HTMLTextAreaElement;
if (textarea) {
addressList = textarea.value;
console.log('从DOM获取的地址列表:', addressList);
}
}
console.log('获取到的地址列表:', addressList);
if (addressList && typeof addressList === 'string') {
const lines = addressList
.split('\n')
.filter((line: string) => line.trim());
const newLineCount = lines.length;
console.log(
'计算出的行数:',
newLineCount,
'当前行数:',
addressLines.value,
);
if (newLineCount !== addressLines.value) {
console.log('定时器更新行数:', newLineCount);
addressLines.value = newLineCount;
}
} else {
console.log('地址列表为空或不是字符串');
}
} catch (error) {
console.error('定时器检查行数失败:', error);
}
}, 300);
};
// 在模态框关闭时清除定时器
const stopLineCheck = () => {
if (lineCheckInterval) {
clearInterval(lineCheckInterval);
lineCheckInterval = null;
}
};
// 缓存刷新按钮处理函数
const handleCacheRefresh = async () => {
try {
cacheRefreshLoading.value = true;
pageLoading.value = true;
loadingTip.value = '缓存刷新中...';
const result = await postDeviceInfoCacheDeviceDataToRedis();
if (result.data) {
Message.success('缓存刷新成功');
gridApi.reload();
} else {
Message.error('缓存刷新失败');
}
} catch (error) {
console.error('缓存刷新失败:', error);
Message.error('缓存刷新失败');
} finally {
cacheRefreshLoading.value = false;
pageLoading.value = false;
loadingTip.value = '缓存刷新中...';
}
};
// 工具栏按钮配置
const toolbarActions = computed(() => [
{
label: $t('common.add'),
type: 'primary',
icon: 'ant-design:plus-outlined',
onClick: openAddModal.bind(null),
auth: ['AbpIdentity.Users.Create'],
},
{
label: '批量添加',
type: 'primary',
icon: 'ant-design:plus-outlined',
onClick: openBatchAddModal.bind(null),
auth: ['AbpIdentity.Users.Create'],
},
{
label: cacheRefreshLoading.value
? $t('common.loading')
: $t('abp.IoTDBBase.CacheRefresh'),
type: 'default',
icon: cacheRefreshLoading.value
? 'ant-design:loading-outlined'
: 'ant-design:reload-outlined',
onClick: handleCacheRefresh,
disabled: cacheRefreshLoading.value,
style: {
backgroundColor: '#52c41a',
borderColor: '#52c41a',
},
},
]);
</script>
<template>
<Page auto-content-height>
<Loading :loading="pageLoading" :tip="loadingTip" />
<Grid>
<template #toolbar-actions>
<TableAction :actions="toolbarActions" />
</template>
<template #isArchiveStatus="{ row }">
{{
row.archiveStatus ? $t('common.Issued') : $t('common.Undistributed')
}}
</template>
<template #isTripState="{ row }">
{{ row.tripState ? $t('common.SwitchOff') : $t('common.Closing') }}
</template>
<template #isHaveValve="{ row }">
<component :is="h(Tag, { color: row.haveValve ? 'green' : 'red' }, () =>
row.haveValve ? $t('common.yes') : $t('common.no'),
)
" />
</template>
<template #isSelfDevelop="{ row }">
<component :is="h(Tag, { color: row.selfDevelop ? 'green' : 'red' }, () =>
row.selfDevelop ? $t('common.yes') : $t('common.no'),
)
" />
</template>
<template #isDynamicPassword="{ row }">
<component :is="h(Tag, { color: row.dynamicPassword ? 'green' : 'red' }, () =>
row.dynamicPassword ? $t('common.yes') : $t('common.no'),
)
" />
</template>
<template #isEnable="{ row }">
<component :is="h(Tag, { color: row.enabled ? 'green' : 'red' }, () =>
row.enabled ? $t('common.yes') : $t('common.no'),
)
" />
</template>
<template #ioTPlatformName="{ row }">
<span :style="{
color:
row.ioTPlatform === 2 || row.ioTPlatform === '2'
? '#0B34BE'
: '#048CD1',
fontWeight: 'bold',
}">
{{ row.ioTPlatformName }}
</span>
</template>
<template #action="{ row }">
<div style="display: flex; gap: 8px; align-items: center">
<!-- 编辑按钮已注释因为后端没有更新接口 -->
<!-- <Button size="small" type="link" @click="onEdit.bind(null, row)()">
{{ $t('common.edit') }}
</Button> -->
<Button size="small" type="link" @click="toDeviceInfoData.bind(null, row)()">
{{ $t('abp.deviceInfos.viewData') }}
</Button>
<Button size="small" type="link" @click="openCommandModal.bind(null, row)()">
{{ $t('abp.IoTDBBase.Command') }}
</Button>
<Popover trigger="hover" placement="bottomRight" :overlay-style="{ minWidth: '120px' }">
<template #content>
<div style="display: flex; flex-direction: column; gap: 4px">
<Button type="text" size="small" @click="toDeviceLog.bind(null, row)()"
style="padding: 4px 8px; text-align: left">
{{ $t('abp.IoTDBBase.PlatformLog') }}
</Button>
<Button type="text" size="small" @click="toTelemetryLog.bind(null, row)()"
style="padding: 4px 8px; text-align: left">
{{ $t('abp.IoTDBBase.TelemetryLog') }}
</Button>
<Button type="text" size="small" @click="repushDeviceInfo.bind(null, row)()"
style="padding: 4px 8px; text-align: left">
重推设备信息
</Button>
<Button type="text" size="small" @click="onDel.bind(null, row)()"
style="padding: 4px 8px; color: #ff4d4f; text-align: left">
{{ $t('common.delete') }}
</Button>
</div>
</template>
<Button size="small" type="link">
<template #icon>
<Icon icon="ant-design:more-outlined" />
</template>
</Button>
</Popover>
</div>
</template>
</Grid>
<UserModal :title="editRow.id ? $t('common.edit') : $t('common.add')" class="w-[800px]">
<component :is="editRow.id ? EditForm : AddForm" />
</UserModal>
<CommandModal :title="$t('abp.IoTDBBase.Command')" class="w-[600px]">
<CommandForm />
</CommandModal>
<BatchAddModal title="批量添加设备" class="w-[800px]">
<BatchAddForm />
<template #footer>
<div class="flex w-full items-center justify-between">
<div class="text-sm text-gray-500">
<span v-if="addressLines > 0">
共 {{ addressLines }} 行设备地址
<span v-if="isOverLineLimit" style="color: #ff4d4f">
(超过100行限制)
</span>
</span>
<span v-else> 当前行数: {{ addressLines }} (调试信息) </span>
</div>
<div class="flex gap-2">
<Button @click="batchAddModalApi.close()">
{{ $t('common.cancel') }}
</Button>
<Button type="primary" :loading="batchAddModalState?.confirmLoading" @click="submitBatchAdd"
:disabled="isOverLineLimit">
{{ $t('common.confirm') }}
</Button>
</div>
</div>
</template>
</BatchAddModal>
</Page>
</template>