Compare commits

..

2 Commits

Author SHA1 Message Date
ChenYi
da54c085ca 批量删除优化 2026-07-20 15:59:09 +08:00
ChenYi
14076375ba 批量删除 2026-07-20 13:52:08 +08:00
5 changed files with 497 additions and 1 deletions

View File

@ -506,6 +506,30 @@ export const BatchCreateDeviceAggregationInputSchema = {
description: '批量创建设备信息'
} as const;
export const BatchDeleteDeviceForApiInputSchema = {
required: ['addressList', 'ioTPlatform', 'ioTPlatformProductId'],
type: 'object',
properties: {
addressList: {
type: 'array',
items: {
type: 'string'
},
description: '设备地址列表'
},
ioTPlatform: {
'$ref': '#/components/schemas/IoTPlatformTypeEnum'
},
ioTPlatformProductId: {
minLength: 1,
type: 'string',
description: '物联网平台中对应的产品Id'
}
},
additionalProperties: false,
description: '批量删除设备入参(只对同一平台、同一产品下的设备批量删除)'
} as const;
export const BatchSyncDeviceFromOneNETInputSchema = {
required: ['ioTPlatform', 'ioTPlatformProductId'],
type: 'object',

File diff suppressed because one or more lines are too long

View File

@ -194,6 +194,21 @@ export type BatchCreateDeviceAggregationInput = {
encryptionType?: DeviceAuthenticationModeEnum;
};
/**
*
*/
export type BatchDeleteDeviceForApiInput = {
/**
*
*/
addressList: Array<(string)>;
ioTPlatform: IoTPlatformTypeEnum;
/**
* Id
*/
ioTPlatformProductId: string;
};
/**
* OneNET批量查询设备并同步本地
*/
@ -7480,6 +7495,19 @@ export type PostAggregationDeviceDeleteAsyncResponse = (boolean);
export type PostAggregationDeviceDeleteAsyncError = unknown;
export type PostAggregationDeviceBatchDeleteAsyncData = {
query?: {
/**
*
*/
input?: BatchDeleteDeviceForApiInput;
};
};
export type PostAggregationDeviceBatchDeleteAsyncResponse = (Array<(string)>);
export type PostAggregationDeviceBatchDeleteAsyncError = unknown;
export type PostAggregationDeviceFindByIdAsyncData = {
query?: {
input?: IdInput;

View File

@ -31,6 +31,7 @@ import { useVbenVxeGrid } from '#/adapter/vxe-table';
import {
getCommonGetSelectList,
postAggregationDeviceBatchCreateAsync,
postAggregationDeviceBatchDeleteAsync,
postAggregationDeviceBatchSyncDeviceFromOneNetAsync,
postAggregationDeviceBindingDeviceThingModel,
postAggregationDeviceCallDeviceServiceForApiAsync,
@ -64,6 +65,7 @@ import DataPushRuleEditor from '#/views/components/DataPushRuleEditor.vue';
import {
addDeviceFormSchema,
batchAddDeviceFormSchema,
batchDeleteDeviceFormSchema,
batchUpgradeDeviceFormSchema,
bindDeviceThingModelFormSchema,
commandFormSchema,
@ -3357,6 +3359,217 @@ async function submitBatchMasterSwitch() {
}
}
// ---------- ----------
// OneNET CTWing EMQX
const BATCH_DELETE_SUPPORTED_PLATFORMS = new Set([IOT_PLATFORM_ONENET]);
const BATCH_DELETE_MAX = 1000;
// " N "
const batchDeleteAddressLines = ref(0);
const isBatchDeleteOverLimit = computed(
() => batchDeleteAddressLines.value > BATCH_DELETE_MAX,
);
function countAddressLines(raw: unknown): number {
if (!raw || typeof raw !== 'string') return 0;
return raw.split('\n').filter((line: string) => line.trim()).length;
}
const [BatchDeleteForm, batchDeleteFormApi] = useVbenForm({
collapsed: false,
commonConfig: {
labelWidth: 110,
componentProps: {
class: 'w-4/5',
},
},
layout: 'horizontal',
schema: batchDeleteDeviceFormSchema.value,
showCollapseButton: false,
showDefaultActions: false,
wrapperClass: 'grid-cols-1',
handleValuesChange: async (values, changedFields) => {
// Id
if (changedFields.includes('ioTPlatformAccountId')) {
await batchDeleteFormApi.setFieldValue('ioTPlatformProductId', undefined);
}
//
if (changedFields.includes('addressList')) {
batchDeleteAddressLines.value = countAddressLines(values.addressList);
}
},
});
const [BatchDeleteModal, batchDeleteModalApi] = useVbenModal({
draggable: true,
onConfirm: submitBatchDeleteFromForm,
onBeforeClose: () => {
batchDeleteFormApi.resetForm();
batchDeleteAddressLines.value = 0;
return true;
},
});
const batchDeleteModalState = batchDeleteModalApi.useStore();
// reload
async function executeBatchDelete(
addressList: string[],
ioTPlatform: number,
ioTPlatformProductId: string,
setLoading: (loading: boolean) => void,
): Promise<boolean> {
try {
setLoading(true);
const { data } = await postAggregationDeviceBatchDeleteAsync({
body: {
addressList,
ioTPlatform,
ioTPlatformProductId,
},
} as any);
const failedList = Array.isArray(data) ? data : [];
const total = addressList.length;
if (failedList.length === 0) {
Message.success(`批量删除成功,共 ${total} 个设备`);
} else {
Modal.warning({
title: '部分设备删除失败',
content: `成功 ${total - failedList.length} 个,失败 ${failedList.length} 个。失败设备地址:${failedList.join('、')}`,
});
}
gridApi.reload();
return failedList.length === 0;
} catch (error) {
console.error('批量删除设备失败:', error);
Message.error('批量删除设备失败');
return false;
} finally {
setLoading(false);
}
}
//
function handleBatchDelete() {
const checkboxRecords = getGridCheckboxRecords();
if (!checkboxRecords || checkboxRecords.length === 0) {
//
batchDeleteFormApi.resetForm();
batchDeleteAddressLines.value = 0;
batchDeleteModalApi.open();
return;
}
//
const productIds = checkboxRecords
.map((r) => r.ioTPlatformProductId)
.filter(Boolean);
if (productIds.length === 0) {
Message.error('选中的设备中没有有效的产品ID无法删除');
return;
}
if (new Set(productIds).size > 1) {
Message.error('选中的设备包含多种产品,请只选择同一产品的设备删除');
return;
}
// OneNET
const platforms = checkboxRecords
.map((r) => toPlatformNumber(r.ioTPlatform))
.filter((p): p is number => p !== undefined);
const uniquePlatforms = [...new Set(platforms)];
if (uniquePlatforms.length > 1) {
Message.error('选中的设备包含多个物联网平台,请只选择同一平台的设备');
return;
}
if (
uniquePlatforms.length === 0 ||
!BATCH_DELETE_SUPPORTED_PLATFORMS.has(uniquePlatforms[0]!)
) {
Message.warning('批量删除仅支持 OneNET 平台的设备');
return;
}
const addressList = checkboxRecords
.map((r) => r.deviceAddress)
.filter(Boolean);
if (addressList.length === 0) {
Message.error('选中的设备缺少设备地址,无法删除');
return;
}
const ioTPlatform = uniquePlatforms[0]!;
const ioTPlatformProductId = String(productIds[0]);
Modal.confirm({
title: '确认批量删除选中设备?',
content: `将删除选中的 ${addressList.length} 个设备,删除后不可恢复。`,
okText: '删除',
okType: 'danger',
cancelText: $t('common.cancel'),
onOk: async () => {
await executeBatchDelete(
addressList,
ioTPlatform,
ioTPlatformProductId,
(loading) => {
pageLoading.value = loading;
loadingTip.value = '批量删除中...';
},
);
},
});
}
// 1000 /
async function submitBatchDeleteFromForm() {
const { valid } = await batchDeleteFormApi.validate();
if (!valid) return;
const formValues = await batchDeleteFormApi.getValues();
const addressList = String(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 > BATCH_DELETE_MAX) {
Message.error(`设备地址不能超过${BATCH_DELETE_MAX}`);
return;
}
const ioTPlatform = toPlatformNumber(formValues.ioTPlatform);
if (ioTPlatform === undefined) {
Message.error('请选择物联网平台');
return;
}
if (!BATCH_DELETE_SUPPORTED_PLATFORMS.has(ioTPlatform)) {
Message.warning('批量删除仅支持 OneNET 平台的设备');
return;
}
const ioTPlatformProductId = formValues.ioTPlatformProductId;
if (!ioTPlatformProductId) {
Message.error('请选择产品');
return;
}
const ok = await executeBatchDelete(
addressList,
ioTPlatform,
String(ioTPlatformProductId),
(loading) =>
batchDeleteModalApi.setState({ loading, confirmLoading: loading }),
);
if (ok) {
batchDeleteModalApi.close();
}
}
//
const toolbarActions = computed(() => [
{
@ -3404,6 +3617,14 @@ const toolbarActions = computed(() => [
onClick: openBatchMasterSwitchModal,
auth: ['AbpIdentity.Users.Create'],
},
{
label: '批量删除',
type: 'primary',
danger: true,
icon: 'ant-design:delete-outlined',
onClick: handleBatchDelete,
auth: ['AbpIdentity.Users.Delete'],
},
{
label: cacheRefreshLoading.value
? $t('common.loading')
@ -3902,6 +4123,39 @@ const [DeviceDataFlowModal, deviceDataFlowModalApi] = useVbenModal({
</div>
</template>
</BatchAddModal>
<BatchDeleteModal title="批量删除设备" class="w-[800px]">
<div class="mb-2 text-sm" style="color: #ff4d4f">
未勾选设备时按此处输入的地址删除仅支持 OneNET 平台最多 1000
删除后不可恢复
</div>
<BatchDeleteForm />
<template #footer>
<div class="flex w-full items-center justify-between">
<div class="text-sm text-gray-500">
<span v-if="batchDeleteAddressLines > 0">
{{ batchDeleteAddressLines }} 行设备地址
<span v-if="isBatchDeleteOverLimit" style="color: #ff4d4f">
(超过{{ BATCH_DELETE_MAX }}行限制)
</span>
</span>
</div>
<div class="flex gap-2">
<Button @click="batchDeleteModalApi.close()">
{{ $t('common.cancel') }}
</Button>
<Button
danger
type="primary"
:disabled="isBatchDeleteOverLimit"
:loading="batchDeleteModalState?.confirmLoading"
@click="submitBatchDeleteFromForm"
>
{{ $t('common.confirm') }}
</Button>
</div>
</div>
</template>
</BatchDeleteModal>
<BatchUpgradeModal title="批量升级设备" class="w-[800px]">
<BatchUpgradeForm />
<template #footer>

View File

@ -1145,6 +1145,186 @@ export const batchAddDeviceFormSchema: any = computed(() => [
},
]);
// 批量删除设备表单(地址 + 平台 + 账号 + 产品;账号仅用于联动查产品,提交时只取平台/产品/地址)
export const batchDeleteDeviceFormSchema: any = computed(() => [
{
component: 'Textarea',
fieldName: 'addressList',
label: $t('abp.deviceInfos.deviceAddress'),
componentProps: {
rows: 4,
placeholder: `${
$t('common.pleaseInput') + $t('abp.deviceInfos.deviceAddress')
}`,
showCount: false,
maxLength: 10_000,
style: {
resize: 'vertical',
minHeight: '32px',
maxHeight: '200px',
},
},
rules: z.string().min(1, {
message: `${$t('common.pleaseInput')}${$t('abp.deviceInfos.deviceAddress')}`,
}),
},
{
component: 'ApiSelect',
fieldName: 'ioTPlatform',
label: $t('abp.deviceInfos.ioTPlatform'),
componentProps: {
api: getCommonGetSelectList,
params: {
query: {
typeName: 'IoTPlatformTypeEnum',
},
},
labelField: 'value',
valueField: 'key',
optionsPropName: 'options',
immediate: true,
allowClear: true,
placeholder:
$t('common.pleaseSelect') + $t('abp.deviceInfos.ioTPlatform'),
afterFetch: (res: any) => {
if (Array.isArray(res)) {
return res;
}
if (res && Array.isArray(res.items)) {
return res.items;
}
if (res && Array.isArray(res.data)) {
return res.data;
}
return [];
},
},
rules: z.string().min(1, {
message: `${$t('common.pleaseSelect')}${$t('abp.deviceInfos.ioTPlatform')}`,
}),
},
{
component: 'ApiSelect',
fieldName: 'ioTPlatformAccountId',
label: $t('abp.deviceInfos.ioTPlatformAccountName'),
dependencies: {
show(values: any) {
return !!values.ioTPlatform;
},
rules(values: any) {
if (values.ioTPlatform) {
return 'required';
}
return null;
},
triggerFields: ['ioTPlatform'],
},
componentProps: (formValues: any) => {
const platform = formValues?.ioTPlatform;
return {
api: platform
? postAggregationIoTplatformGetIoTplatformAccountInfoAsync
: null,
params: platform
? {
body: {
ioTPlatformType: getAccountQueryPlatformType(platform),
},
}
: {},
labelField: 'ioTPlatformPhoneNumber',
valueField: 'ioTPlatformAccountId',
optionsPropName: 'options',
immediate: false,
allowClear: true,
placeholder:
$t('common.pleaseSelect') +
$t('abp.deviceInfos.ioTPlatformAccountName'),
afterFetch: (res: any) => {
let items: any[] = [];
if (Array.isArray(res)) {
items = res;
} else if (res && Array.isArray(res.items)) {
items = res.items;
} else if (res && Array.isArray(res.data)) {
items = res.data;
} else if (res && res.data && Array.isArray(res.data.items)) {
items = res.data.items;
}
return items.map((item: any) => ({
...item,
ioTPlatformAccountId: item.ioTPlatformAccount,
label: item.ioTPlatformPhoneNumber || item.ioTPlatformAccount || '',
}));
},
};
},
rules: z.string().optional(),
},
{
component: 'ApiSelect',
fieldName: 'ioTPlatformProductId',
label: $t('abp.deviceInfos.ioTPlatformProductName'),
dependencies: {
show(values: any) {
return !!values.ioTPlatform && !!values.ioTPlatformAccountId;
},
rules(values: any) {
if (values.ioTPlatform && values.ioTPlatformAccountId) {
return 'required';
}
return null;
},
triggerFields: ['ioTPlatform', 'ioTPlatformAccountId'],
},
componentProps: (formValues: any) => {
const platform = formValues?.ioTPlatform;
const accountId = formValues?.ioTPlatformAccountId;
return {
api:
platform && accountId
? postAggregationIoTplatformGetIoTplatformProductInfoAsync
: null,
params:
platform && accountId
? {
body: {
ioTPlatformType: getAccountQueryPlatformType(platform),
ioTPlatformAccount: accountId,
},
}
: {},
labelField: 'productName',
valueField: 'ioTPlatformProductId',
optionsPropName: 'options',
immediate: false,
allowClear: true,
placeholder:
$t('common.pleaseSelect') +
$t('abp.deviceInfos.ioTPlatformProductName'),
afterFetch: (res: any) => {
if (Array.isArray(res)) {
return res;
}
if (res && Array.isArray(res.items)) {
return res.items;
}
if (res && Array.isArray(res.data)) {
return res.data;
}
if (res && res.data && Array.isArray(res.data.items)) {
return res.data.items;
}
return [];
},
};
},
rules: z.string().optional(),
},
]);
// 批量设备升级表单
export const batchUpgradeDeviceFormSchema: any = computed(() => [
{