Compare commits
2 Commits
3454d227db
...
da54c085ca
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da54c085ca | ||
|
|
14076375ba |
@ -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
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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(() => [
|
||||
{
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user