Commit b984eeb26e16616ecf71397e68b0c6d7fe247df0

Authored by 温伟
Committed by xp.Huang
1 parent 08551861

feat:支持设备批量更新产品,优化批量操作按钮

@@ -51,6 +51,11 @@ enum DeviceManagerApi { @@ -51,6 +51,11 @@ enum DeviceManagerApi {
51 * @description 通过设备列表获取设备信息 51 * @description 通过设备列表获取设备信息
52 */ 52 */
53 QUERY_DEVICES = '/device/get/devices', 53 QUERY_DEVICES = '/device/get/devices',
  54 +
  55 + /**
  56 + * @description 批量变更产品
  57 + */
  58 + BATCH_UPDATE_PRODUCT = '/device/batch/update',
54 } 59 }
55 60
56 export const devicePage = (params: DeviceQueryParam) => { 61 export const devicePage = (params: DeviceQueryParam) => {
@@ -378,3 +383,13 @@ export const doBatchClearAlarm = (ids: string[]) => { @@ -378,3 +383,13 @@ export const doBatchClearAlarm = (ids: string[]) => {
378 { joinPrefix: false } 383 { joinPrefix: false }
379 ); 384 );
380 }; 385 };
  386 +
  387 +export const doBatchUpdateProduct = (params: { deviceIds: string[]; deviceProfileId: string }) => {
  388 + return defHttp.post(
  389 + {
  390 + url: DeviceManagerApi.BATCH_UPDATE_PRODUCT,
  391 + data: params,
  392 + },
  393 + { joinPrefix: false }
  394 + );
  395 +};
@@ -54,7 +54,7 @@ @@ -54,7 +54,7 @@
54 }, 54 },
55 // api params 55 // api params
56 params: { 56 params: {
57 - type: Object as PropType<Recordable>, 57 + type: [Object, String, Number] as any,
58 default: () => ({}), 58 default: () => ({}),
59 }, 59 },
60 // support xxx.xxx.xx 60 // support xxx.xxx.xx
@@ -44,7 +44,7 @@ @@ -44,7 +44,7 @@
44 </ModalWrapper> 44 </ModalWrapper>
45 45
46 <template #[item]="data" v-for="item in Object.keys(omit($slots, 'default'))"> 46 <template #[item]="data" v-for="item in Object.keys(omit($slots, 'default'))">
47 - <slot :name="item" v-bind="data"></slot> 47 + <slot :name="item" v-bind="data || {}"></slot>
48 </template> 48 </template>
49 </Modal> 49 </Modal>
50 </template> 50 </template>
1 import { FormProps, FormSchema, useComponentRegister } from '/@/components/Form'; 1 import { FormProps, FormSchema, useComponentRegister } from '/@/components/Form';
2 import { findDictItemByCode } from '/@/api/system/dict'; 2 import { findDictItemByCode } from '/@/api/system/dict';
3 -import { deviceProfile, getGatewayDevice } from '/@/api/device/deviceManager'; 3 +import { getGatewayDevice, queryDeviceProfileBy } from '/@/api/device/deviceManager';
4 import { TransportTypeEnum } from '../../profiles/components/TransportDescript/const'; 4 import { TransportTypeEnum } from '../../profiles/components/TransportDescript/const';
5 import { JSONEditorValidator } from '/@/components/CodeEditor/src/JSONEditor'; 5 import { JSONEditorValidator } from '/@/components/CodeEditor/src/JSONEditor';
6 import { JSONEditor } from '/@/components/CodeEditor'; 6 import { JSONEditor } from '/@/components/CodeEditor';
@@ -96,15 +96,26 @@ export const step1Schemas: FormSchema[] = [ @@ -96,15 +96,26 @@ export const step1Schemas: FormSchema[] = [
96 show: false, 96 show: false,
97 }, 97 },
98 { 98 {
  99 + field: 'isUpdate',
  100 + label: '编辑模式',
  101 + component: 'Switch',
  102 + show: false,
  103 + },
  104 + {
99 field: 'profileId', 105 field: 'profileId',
100 label: '所属产品', 106 label: '所属产品',
101 required: true, 107 required: true,
102 component: 'ApiSelect', 108 component: 'ApiSelect',
  109 + helpMessage: [
  110 + '注意:修改设备产品时,如果"大屏"、"组态"、"看板"、"场景联动"、"数据流转"有关联设备,请自行调整并保存。',
  111 + ],
103 componentProps: ({ formActionType, formModel }) => { 112 componentProps: ({ formActionType, formModel }) => {
104 const { setFieldsValue } = formActionType; 113 const { setFieldsValue } = formActionType;
105 return { 114 return {
106 api: async () => { 115 api: async () => {
107 - const options = await deviceProfile(); 116 + const options = await queryDeviceProfileBy({
  117 + deviceType: formModel?.isUpdate ? formModel?.deviceType : null,
  118 + });
108 const { profileId } = formModel; 119 const { profileId } = formModel;
109 if (profileId) { 120 if (profileId) {
110 const selectRecord = options.find((item) => item.tbProfileId === profileId); 121 const selectRecord = options.find((item) => item.tbProfileId === profileId);
@@ -7,6 +7,63 @@ import { h } from 'vue'; @@ -7,6 +7,63 @@ import { h } from 'vue';
7 import { Tag, Tooltip } from 'ant-design-vue'; 7 import { Tag, Tooltip } from 'ant-design-vue';
8 import { handeleCopy } from '../../profiles/step/topic'; 8 import { handeleCopy } from '../../profiles/step/topic';
9 9
  10 +export enum DeviceListAuthEnum {
  11 + /**
  12 + * @description 新增
  13 + */
  14 + CREATE = 'api:yt:device:post',
  15 +
  16 + /**
  17 + * @description 删除
  18 + */
  19 + DELETE = 'api:yt:device:delete',
  20 +
  21 + /**
  22 + * @description 编辑
  23 + */
  24 + UPDATE = 'api:yt:device:update',
  25 +
  26 + /**
  27 + * @description 详情
  28 + */
  29 + DETAIL = 'api:yt:device:get',
  30 +
  31 + /**
  32 + * @description 导入
  33 + */
  34 + IMPORT = 'api:yt:device:import',
  35 +
  36 + /**
  37 + * @description 公开
  38 + */
  39 + PUBLIC = 'api:yt:device:public',
  40 +
  41 + /**
  42 + * @description 上下线
  43 + */
  44 + ONLINE = 'api:yt:device:online:record',
  45 +
  46 + /**
  47 + * @description 管理设备凭证
  48 + */
  49 + EQUIPMENT = 'api:yt:device:equipment',
  50 +
  51 + /**
  52 + * @description 分配客户
  53 + */
  54 + ASSIGN = 'api:yt:device:assign',
  55 +
  56 + /**
  57 + * @description 命令下发
  58 + */
  59 + RPC = 'api:yt:device:rpc',
  60 +
  61 + /**
  62 + * @description 更新产品
  63 + */
  64 + UPDATE_PRODUCT = 'api:yt:device:update:product',
  65 +}
  66 +
10 // 表格列数据 67 // 表格列数据
11 export const columns: BasicColumn[] = [ 68 export const columns: BasicColumn[] = [
12 { 69 {
  1 +import { getDeviceProfile } from '/@/api/alarm/position';
  2 +import { FormSchema } from '/@/components/Form';
  3 +
  4 +enum FormFieldsEnum {
  5 + DEVICE_TYPE = 'deviceType',
  6 + SOURCE_DEVICE_PROFILE_ID = 'sourceDeviceProfileName',
  7 + TARGET_DEVICE_PROFILE_ID = 'deviceProfileId',
  8 +}
  9 +
  10 +export type FormGetFiledValueResultType = Record<FormFieldsEnum, string>;
  11 +
  12 +export const formSchemas: FormSchema[] = [
  13 + {
  14 + field: FormFieldsEnum.SOURCE_DEVICE_PROFILE_ID,
  15 + component: 'Input',
  16 + label: '源产品',
  17 + dynamicDisabled: true,
  18 + required: true,
  19 + },
  20 + {
  21 + field: FormFieldsEnum.DEVICE_TYPE,
  22 + label: '设备类型',
  23 + component: 'Input',
  24 + show: false,
  25 + },
  26 + {
  27 + field: FormFieldsEnum.TARGET_DEVICE_PROFILE_ID,
  28 + component: 'ApiSelect',
  29 + label: '目标产品',
  30 + required: true,
  31 + componentProps: ({ formModel }) => {
  32 + return {
  33 + api: getDeviceProfile,
  34 + params: formModel[FormFieldsEnum.DEVICE_TYPE],
  35 + labelField: 'name',
  36 + valueField: 'tbProfileId',
  37 + placeholder: '请选择目标产品',
  38 + getPopupContainer: () => document.body,
  39 + };
  40 + },
  41 + },
  42 +];
  1 +export { default as BatchUpdateProductModal } from './index.vue';
  2 +
  3 +export type BatchUpdateProductModalParamsType = ModalParamsType<
  4 + Record<'sourceDeviceProfileName' | 'deviceType', string> & Record<'deviceIds', string[]>
  5 +>;
  1 +<script setup lang="ts">
  2 + import { ref, unref } from 'vue';
  3 + import { BatchUpdateProductModalParamsType } from '.';
  4 + import { FormGetFiledValueResultType, formSchemas } from './config';
  5 + import { doBatchUpdateProduct } from '/@/api/device/deviceManager';
  6 + import { BasicForm, useForm } from '/@/components/Form';
  7 + import Icon from '/@/components/Icon';
  8 + import { BasicModal, useModalInner } from '/@/components/Modal';
  9 + import { useMessage } from '/@/hooks/web/useMessage';
  10 +
  11 + const emits = defineEmits(['register', 'success']);
  12 +
  13 + const deviceIds = ref<string[]>([]);
  14 + const [registerForm, { validate, getFieldsValue, setFieldsValue, resetFields }] = useForm({
  15 + layout: 'vertical',
  16 + showActionButtonGroup: false,
  17 + schemas: formSchemas,
  18 + });
  19 +
  20 + const [registerModal, { setModalProps, closeModal }] = useModalInner(
  21 + async ({ record }: BatchUpdateProductModalParamsType) => {
  22 + resetFields();
  23 + deviceIds.value = record.deviceIds;
  24 + setFieldsValue(record);
  25 + }
  26 + );
  27 +
  28 + const { createMessage } = useMessage();
  29 + const handleOk = async () => {
  30 + try {
  31 + await validate();
  32 + setModalProps({ loading: true, confirmLoading: true });
  33 +
  34 + const result = getFieldsValue() as FormGetFiledValueResultType;
  35 +
  36 + await doBatchUpdateProduct({
  37 + deviceIds: unref(deviceIds),
  38 + deviceProfileId: result.deviceProfileId,
  39 + });
  40 +
  41 + createMessage.success('操作成功');
  42 +
  43 + closeModal();
  44 + emits('success');
  45 + } finally {
  46 + setModalProps({ loading: false, confirmLoading: false });
  47 + }
  48 + };
  49 +</script>
  50 +
  51 +<template>
  52 + <BasicModal @register="registerModal" @ok="handleOk">
  53 + <template #title>
  54 + <div>批量更新产品</div>
  55 + <div class="mt-4 font-normal text-sm">
  56 + <Icon icon="ant-design:info-circle-outlined" />
  57 + <span>
  58 + 注意:修改设备产品时只能修改为同类型产品,如果"大屏"、"组态"、"看板"、"场景联动"、"数据流转"等有关联设备,请自行调整并保存。
  59 + </span>
  60 + </div>
  61 + </template>
  62 + <!-- <template #title>
  63 + <span></span>
  64 + </template> -->
  65 + <BasicForm @register="registerForm" />
  66 + </BasicModal>
  67 +</template>
@@ -20,7 +20,7 @@ @@ -20,7 +20,7 @@
20 @next="handleStep1Next" 20 @next="handleStep1Next"
21 ref="DeviceStep1Ref" 21 ref="DeviceStep1Ref"
22 v-show="current === 0" 22 v-show="current === 0"
23 - :isUpdate="!isUpdate" 23 + :isUpdate="isUpdate"
24 /> 24 />
25 <DeviceStep2 25 <DeviceStep2
26 ref="DeviceStep2Ref" 26 ref="DeviceStep2Ref"
@@ -33,7 +33,7 @@ @@ -33,7 +33,7 @@
33 </BasicModal> 33 </BasicModal>
34 </template> 34 </template>
35 <script lang="ts"> 35 <script lang="ts">
36 - import { defineComponent, ref, computed, unref } from 'vue'; 36 + import { defineComponent, ref, computed, unref, nextTick } from 'vue';
37 import { BasicModal, useModalInner } from '/@/components/Modal'; 37 import { BasicModal, useModalInner } from '/@/components/Modal';
38 import { createOrEditDevice } from '/@/api/device/deviceManager'; 38 import { createOrEditDevice } from '/@/api/device/deviceManager';
39 import DeviceStep1 from '../step/DeviceStep1.vue'; 39 import DeviceStep1 from '../step/DeviceStep1.vue';
@@ -60,16 +60,17 @@ @@ -60,16 +60,17 @@
60 const DeviceStep2Ref = ref<InstanceType<typeof DeviceStep2>>(); 60 const DeviceStep2Ref = ref<InstanceType<typeof DeviceStep2>>();
61 const { createMessage } = useMessage(); 61 const { createMessage } = useMessage();
62 const current = ref(0); 62 const current = ref(0);
63 - const isUpdate = ref<Boolean>(false); 63 + const isUpdate = ref(false);
64 const deviceInfo = ref({}); 64 const deviceInfo = ref({});
65 let currentDeviceData = {} as Recordable; 65 let currentDeviceData = {} as Recordable;
66 const getTitle = computed(() => (!unref(isUpdate) ? '新增设备' : '编辑设备')); 66 const getTitle = computed(() => (!unref(isUpdate) ? '新增设备' : '编辑设备'));
67 // 所有参数 67 // 所有参数
68 let stepState = ref(); 68 let stepState = ref();
69 // 编辑回显 69 // 编辑回显
70 - const [register, { closeModal, setModalProps }] = useModalInner((data) => { 70 + const [register, { closeModal, setModalProps }] = useModalInner(async (data) => {
71 setModalProps({ confirmLoading: false, loading: true }); 71 setModalProps({ confirmLoading: false, loading: true });
72 isUpdate.value = data?.isUpdate; 72 isUpdate.value = data?.isUpdate;
  73 + await nextTick();
73 if (unref(isUpdate)) { 74 if (unref(isUpdate)) {
74 const { record } = data; 75 const { record } = data;
75 currentDeviceData = record; 76 currentDeviceData = record;
@@ -36,7 +36,7 @@ @@ -36,7 +36,7 @@
36 </Input> 36 </Input>
37 </template> 37 </template>
38 </BasicForm> 38 </BasicForm>
39 - <div class="flex justify-center" v-if="isUpdate"> 39 + <div class="flex justify-center" v-if="!isUpdate">
40 <a-button type="primary" @click="nextStep">下一步</a-button> 40 <a-button type="primary" @click="nextStep">下一步</a-button>
41 </div> 41 </div>
42 </div> 42 </div>
@@ -199,6 +199,7 @@ @@ -199,6 +199,7 @@
199 showResetButton: false, 199 showResetButton: false,
200 showSubmitButton: false, 200 showSubmitButton: false,
201 }); 201 });
  202 +
202 async function nextStep() { 203 async function nextStep() {
203 try { 204 try {
204 let values = await validate(); 205 let values = await validate();
@@ -245,7 +246,7 @@ @@ -245,7 +246,7 @@
245 const selectPosition = async () => { 246 const selectPosition = async () => {
246 visible.value = true; 247 visible.value = true;
247 if ( 248 if (
248 - !unref(isUpdate1) && 249 + unref(isUpdate1) &&
249 unref(devicePositionState).longitude && 250 unref(devicePositionState).longitude &&
250 unref(devicePositionState).latitude 251 unref(devicePositionState).latitude
251 ) { 252 ) {
@@ -440,8 +441,10 @@ @@ -440,8 +441,10 @@
440 positionState.address = deviceInfo.address; 441 positionState.address = deviceInfo.address;
441 devicePositionState.value = { ...toRaw(positionState) }; 442 devicePositionState.value = { ...toRaw(positionState) };
442 devicePic.value = deviceInfo.avatar; 443 devicePic.value = deviceInfo.avatar;
  444 +
443 if (deviceInfo.avatar) { 445 if (deviceInfo.avatar) {
444 setFieldsValue({ 446 setFieldsValue({
  447 + isUpdate: unref(isUpdate1),
445 icon: [{ uid: buildUUID(), name: 'name', url: deviceInfo.avatar } as FileItem], 448 icon: [{ uid: buildUUID(), name: 'name', url: deviceInfo.avatar } as FileItem],
446 }); 449 });
447 } 450 }
@@ -449,6 +452,7 @@ @@ -449,6 +452,7 @@
449 ...data, 452 ...data,
450 code: data?.code, 453 code: data?.code,
451 addressCode: parseInt(data?.code || '', 16), 454 addressCode: parseInt(data?.code || '', 16),
  455 + isUpdate: unref(isUpdate1),
452 }); 456 });
453 } 457 }
454 // 父组件调用获取字段值的方法 458 // 父组件调用获取字段值的方法
@@ -4,41 +4,57 @@ @@ -4,41 +4,57 @@
4 <OrganizationIdTree @select="handleSelect" ref="organizationIdTreeRef" /> 4 <OrganizationIdTree @select="handleSelect" ref="organizationIdTreeRef" />
5 <BasicTable style="flex: auto" @register="registerTable" class="w-5/6 xl:w-4/5"> 5 <BasicTable style="flex: auto" @register="registerTable" class="w-5/6 xl:w-4/5">
6 <template #toolbar> 6 <template #toolbar>
7 - <Authority value="api:yt:device:post"> 7 + <Authority :value="DeviceListAuthEnum.CREATE">
8 <a-button type="primary" @click="handleCreate" v-if="authBtn(role)"> 8 <a-button type="primary" @click="handleCreate" v-if="authBtn(role)">
9 新增设备 9 新增设备
10 </a-button> 10 </a-button>
11 </Authority> 11 </Authority>
12 - <Authority value="api:yt:device:delete">  
13 - <Popconfirm  
14 - title="您确定要批量删除数据"  
15 - ok-text="确定"  
16 - cancel-text="取消"  
17 - @confirm="handleDelete()"  
18 - >  
19 - <a-button color="error" v-if="authBtn(role)" :disabled="!isExistOption">  
20 - 批量删除  
21 - </a-button>  
22 - </Popconfirm>  
23 - </Authority>  
24 - <Authority value="api:yt:device:import"> 12 +
  13 + <Authority :value="DeviceListAuthEnum.IMPORT">
25 <Button type="primary" @click="handleBatchImport">导入</Button> 14 <Button type="primary" @click="handleBatchImport">导入</Button>
26 </Authority> 15 </Authority>
27 - <Authority value="api:yt:device:assign">  
28 - <a-button 16 +
  17 + <Authority
  18 + :value="[
  19 + DeviceListAuthEnum.DELETE,
  20 + DeviceListAuthEnum.ASSIGN,
  21 + DeviceListAuthEnum.UPDATE_PRODUCT,
  22 + ]"
  23 + >
  24 + <AuthDropDown
29 v-if="authBtn(role)" 25 v-if="authBtn(role)"
30 - type="primary"  
31 - @click="handleBatchAssign"  
32 :disabled="!isExistOption" 26 :disabled="!isExistOption"
  27 + :dropMenuList="[
  28 + {
  29 + text: '删除设备',
  30 + auth: DeviceListAuthEnum.DELETE,
  31 + icon: 'ant-design:delete-outlined',
  32 + event: '',
  33 + popconfirm: {
  34 + title: '您确定要批量删除数据',
  35 + onConfirm: () => handleDelete(),
  36 + },
  37 + },
  38 + {
  39 + text: '分配客户',
  40 + auth: DeviceListAuthEnum.ASSIGN,
  41 + icon: 'mdi:account-arrow-left',
  42 + event: '',
  43 + onClick: handleBatchAssign.bind(null),
  44 + },
  45 + {
  46 + text: '更新产品',
  47 + auth: DeviceListAuthEnum.UPDATE_PRODUCT,
  48 + icon: 'clarity:note-edit-line',
  49 + event: '',
  50 + disabled: batchUpdateProductFlag,
  51 + onClick: handelOpenBatchUpdateProductModal,
  52 + },
  53 + ]"
33 > 54 >
34 - 批量分配  
35 - </a-button> 55 + <Button type="primary" :disabled="!isExistOption">批量操作</Button>
  56 + </AuthDropDown>
36 </Authority> 57 </Authority>
37 - <!-- <Authority>  
38 - <a-button type="primary" @click="handelCollect()" :disabled="!isExistOption">  
39 - 批量收藏  
40 - </a-button>  
41 - </Authority> -->  
42 </template> 58 </template>
43 <template #img="{ record }"> 59 <template #img="{ record }">
44 <TableImg 60 <TableImg
@@ -121,12 +137,12 @@ @@ -121,12 +137,12 @@
121 { 137 {
122 label: '详情', 138 label: '详情',
123 icon: 'ant-design:eye-outlined', 139 icon: 'ant-design:eye-outlined',
124 - auth: 'api:yt:device:get', 140 + auth: DeviceListAuthEnum.DETAIL,
125 onClick: handleDetail.bind(null, record), 141 onClick: handleDetail.bind(null, record),
126 }, 142 },
127 { 143 {
128 label: '编辑', 144 label: '编辑',
129 - auth: 'api:yt:device:update', 145 + auth: DeviceListAuthEnum.UPDATE,
130 icon: 'clarity:note-edit-line', 146 icon: 'clarity:note-edit-line',
131 ifShow: authBtn(role), 147 ifShow: authBtn(role),
132 onClick: handleEdit.bind(null, record), 148 onClick: handleEdit.bind(null, record),
@@ -138,7 +154,7 @@ @@ -138,7 +154,7 @@
138 label: '取消分配', 154 label: '取消分配',
139 icon: 'mdi:account-arrow-left', 155 icon: 'mdi:account-arrow-left',
140 ifShow: authBtn(role) && !record?.customerAdditionalInfo?.isPublic, 156 ifShow: authBtn(role) && !record?.customerAdditionalInfo?.isPublic,
141 - auth: 'api:yt:device:assign', 157 + auth: DeviceListAuthEnum.ASSIGN,
142 popConfirm: { 158 popConfirm: {
143 title: '是否取消分配客户', 159 title: '是否取消分配客户',
144 confirm: handleCancelDispatchCustomer.bind(null, record), 160 confirm: handleCancelDispatchCustomer.bind(null, record),
@@ -148,12 +164,12 @@ @@ -148,12 +164,12 @@
148 label: '分配客户', 164 label: '分配客户',
149 icon: 'mdi:account-arrow-right', 165 icon: 'mdi:account-arrow-right',
150 ifShow: authBtn(role), 166 ifShow: authBtn(role),
151 - auth: 'api:yt:device:assign', 167 + auth: DeviceListAuthEnum.ASSIGN,
152 onClick: handleDispatchCustomer.bind(null, record), 168 onClick: handleDispatchCustomer.bind(null, record),
153 }, 169 },
154 { 170 {
155 label: record?.customerAdditionalInfo?.isPublic ? '私有' : '公开', 171 label: record?.customerAdditionalInfo?.isPublic ? '私有' : '公开',
156 - auth: 'api:yt:device:public', 172 + auth: DeviceListAuthEnum.PUBLIC,
157 icon: record?.customerAdditionalInfo?.isPublic 173 icon: record?.customerAdditionalInfo?.isPublic
158 ? 'ant-design:lock-outlined' 174 ? 'ant-design:lock-outlined'
159 : 'ant-design:unlock-outlined', 175 : 'ant-design:unlock-outlined',
@@ -161,7 +177,7 @@ @@ -161,7 +177,7 @@
161 }, 177 },
162 { 178 {
163 label: '上下线记录', 179 label: '上下线记录',
164 - auth: 'api:yt:device:online:record', 180 + auth: DeviceListAuthEnum.ONLINE,
165 icon: 'ant-design:rise-outlined', 181 icon: 'ant-design:rise-outlined',
166 onClick: handleUpAndDownRecord.bind(null, record), 182 onClick: handleUpAndDownRecord.bind(null, record),
167 }, 183 },
@@ -173,7 +189,6 @@ @@ -173,7 +189,6 @@
173 } 189 }
174 : { 190 : {
175 label: '取消收藏', 191 label: '取消收藏',
176 - auth: 'api:yt:device:online:record',  
177 icon: 'ant-design:heart-outlined', 192 icon: 'ant-design:heart-outlined',
178 popConfirm: { 193 popConfirm: {
179 title: '是否取消收藏', 194 title: '是否取消收藏',
@@ -182,7 +197,7 @@ @@ -182,7 +197,7 @@
182 }, 197 },
183 { 198 {
184 label: '删除', 199 label: '删除',
185 - auth: 'api:yt:device:delete', 200 + auth: DeviceListAuthEnum.DELETE,
186 icon: 'ant-design:delete-outlined', 201 icon: 'ant-design:delete-outlined',
187 ifShow: authBtn(role) && record.customerId === undefined, 202 ifShow: authBtn(role) && record.customerId === undefined,
188 color: 'error', 203 color: 'error',
@@ -208,11 +223,16 @@ @@ -208,11 +223,16 @@
208 <CustomerModal @register="registerCustomerModal" @reload="handleReload" /> 223 <CustomerModal @register="registerCustomerModal" @reload="handleReload" />
209 224
210 <BatchImportModal @register="registerImportModal" @import-finally="handleImportFinally" /> 225 <BatchImportModal @register="registerImportModal" @import-finally="handleImportFinally" />
  226 +
  227 + <BatchUpdateProductModal
  228 + @register="registerBatchUpdateProductModal"
  229 + @success="handleBatchUpdateProductSuccess"
  230 + />
211 </PageWrapper> 231 </PageWrapper>
212 </div> 232 </div>
213 </template> 233 </template>
214 -<script lang="ts">  
215 - import { defineComponent, reactive, unref, onMounted } from 'vue'; 234 +<script lang="ts" setup>
  235 + import { reactive, onMounted, ref } from 'vue';
216 import { 236 import {
217 DeviceModel, 237 DeviceModel,
218 DeviceRecord, 238 DeviceRecord,
@@ -220,8 +240,8 @@ @@ -220,8 +240,8 @@
220 DeviceTypeEnum, 240 DeviceTypeEnum,
221 } from '/@/api/device/model/deviceModel'; 241 } from '/@/api/device/model/deviceModel';
222 import { BasicTable, useTable, TableAction, TableImg } from '/@/components/Table'; 242 import { BasicTable, useTable, TableAction, TableImg } from '/@/components/Table';
223 - import { columns, searchFormSchema } from './config/device.data';  
224 - import { Tag, Popover, Popconfirm, Button, Tooltip } from 'ant-design-vue'; 243 + import { columns, DeviceListAuthEnum, searchFormSchema } from './config/device.data';
  244 + import { Tag, Popover, Button, Tooltip } from 'ant-design-vue';
225 import { HeartTwoTone } from '@ant-design/icons-vue'; 245 import { HeartTwoTone } from '@ant-design/icons-vue';
226 import { 246 import {
227 deleteDevice, 247 deleteDevice,
@@ -246,326 +266,295 @@ @@ -246,326 +266,295 @@
246 import { USER_INFO_KEY } from '/@/enums/cacheEnum'; 266 import { USER_INFO_KEY } from '/@/enums/cacheEnum';
247 import { getAuthCache } from '/@/utils/auth'; 267 import { getAuthCache } from '/@/utils/auth';
248 import { authBtn } from '/@/enums/roleEnum'; 268 import { authBtn } from '/@/enums/roleEnum';
249 - import { useClipboard } from '@vueuse/core';  
250 import { QuestionCircleOutlined } from '@ant-design/icons-vue'; 269 import { QuestionCircleOutlined } from '@ant-design/icons-vue';
251 import { Authority } from '/@/components/Authority'; 270 import { Authority } from '/@/components/Authority';
252 import { useRoute, useRouter } from 'vue-router'; 271 import { useRoute, useRouter } from 'vue-router';
253 import { useBatchOperation } from '/@/utils/useBatchOperation'; 272 import { useBatchOperation } from '/@/utils/useBatchOperation';
254 import { useSyncConfirm } from '/@/hooks/component/useSyncConfirm'; 273 import { useSyncConfirm } from '/@/hooks/component/useSyncConfirm';
255 import { useAuthDeviceDetail } from './hook/useAuthDeviceDetail'; 274 import { useAuthDeviceDetail } from './hook/useAuthDeviceDetail';
  275 + import {
  276 + BatchUpdateProductModal,
  277 + BatchUpdateProductModalParamsType,
  278 + } from './cpns/modal/BatchUpdateProductModal';
  279 + import { DataActionModeEnum } from '/@/enums/toolEnum';
  280 + import { AuthDropDown } from '/@/components/Widget';
256 281
257 - export default defineComponent({  
258 - name: 'DeviceManagement',  
259 - components: {  
260 - BasicTable,  
261 - PageWrapper,  
262 - TableAction,  
263 - OrganizationIdTree,  
264 - Tag,  
265 - DeviceModal,  
266 - DeviceDetailDrawer,  
267 - CustomerModal,  
268 - TableImg,  
269 - QuestionCircleOutlined,  
270 - Popover,  
271 - Authority,  
272 - Popconfirm,  
273 - BatchImportModal,  
274 - Button,  
275 - // HeartOutlined,  
276 - HeartTwoTone,  
277 - Tooltip,  
278 - },  
279 - setup(_) {  
280 - const { isCustomer } = useAuthDeviceDetail();  
281 - const { createMessage } = useMessage();  
282 - const go = useGo();  
283 - const ROUTER = useRouter();  
284 - const ROUTE = useRoute();  
285 - const searchInfo = reactive<Recordable>({});  
286 - const { organizationIdTreeRef, resetFn } = useResetOrganizationTree(searchInfo);  
287 - const [registerModal, { openModal }] = useModal();  
288 - const [registerCustomerModal, { openModal: openCustomerModal }] = useModal();  
289 - const [registerDetailDrawer, { openDrawer }] = useDrawer();  
290 - const [registerTbDetailDrawer, { openDrawer: openTbDeviceDrawer }] = useDrawer();  
291 - const [registerGatewayDetailDrawer, { openDrawer: openGatewayDetailDrawer }] = useDrawer();  
292 - const [registerImportModal, { openModal: openImportModal }] = useModal(); 282 + const { isCustomer } = useAuthDeviceDetail();
  283 + const { createMessage } = useMessage();
  284 + const go = useGo();
  285 + const ROUTER = useRouter();
  286 + const ROUTE = useRoute();
  287 + const searchInfo = reactive<Recordable>({});
  288 + const { organizationIdTreeRef, resetFn } = useResetOrganizationTree(searchInfo);
  289 + const [registerModal, { openModal }] = useModal();
  290 + const [registerCustomerModal, { openModal: openCustomerModal }] = useModal();
  291 + const [registerDetailDrawer, { openDrawer }] = useDrawer();
  292 + const [registerTbDetailDrawer, { openDrawer: openTbDeviceDrawer }] = useDrawer();
  293 + const [registerGatewayDetailDrawer, { openDrawer: openGatewayDetailDrawer }] = useDrawer();
  294 + const [registerImportModal, { openModal: openImportModal }] = useModal();
  295 + const [registerBatchUpdateProductModal, { openModal: openBatchUpdateProductModal }] = useModal();
293 296
294 - const [  
295 - registerTable,  
296 - {  
297 - reload,  
298 - setLoading,  
299 - setSelectedRowKeys,  
300 - getForm,  
301 - getSelectRowKeys,  
302 - getSelectRows,  
303 - getRowSelection,  
304 - },  
305 - ] = useTable({  
306 - title: '设备列表',  
307 - api: devicePage,  
308 - columns,  
309 - beforeFetch: (params) => {  
310 - const { deviceProfileId } = params;  
311 - if (!deviceProfileId) return;  
312 - const obj = {  
313 - ...params,  
314 - ...{  
315 - deviceProfileIds: deviceProfileId ? [deviceProfileId] : null,  
316 - },  
317 - };  
318 - delete obj.deviceProfileId;  
319 - return obj;  
320 - },  
321 - formConfig: {  
322 - labelWidth: 100,  
323 - schemas: searchFormSchema,  
324 - resetFunc: resetFn,  
325 - },  
326 - useSearchForm: true,  
327 - showTableSetting: true,  
328 - bordered: true,  
329 - showIndexColumn: false,  
330 - rowKey: 'id',  
331 - searchInfo: searchInfo,  
332 - clickToRowSelect: false,  
333 - actionColumn: {  
334 - width: 200,  
335 - title: '操作',  
336 - slots: { customRender: 'action' },  
337 - fixed: 'right',  
338 - },  
339 - rowSelection: {  
340 - type: 'checkbox',  
341 - getCheckboxProps: (record: DeviceModel) => {  
342 - return { disabled: !!record.customerId };  
343 - }, 297 + const batchUpdateProductFlag = ref(true);
  298 +
  299 + const [
  300 + registerTable,
  301 + {
  302 + reload,
  303 + setLoading,
  304 + setSelectedRowKeys,
  305 + getForm,
  306 + getSelectRowKeys,
  307 + getSelectRows,
  308 + getRowSelection,
  309 + clearSelectedRowKeys,
  310 + },
  311 + ] = useTable({
  312 + title: '设备列表',
  313 + api: devicePage,
  314 + columns,
  315 + beforeFetch: (params) => {
  316 + const { deviceProfileId } = params;
  317 + if (!deviceProfileId) return;
  318 + const obj = {
  319 + ...params,
  320 + ...{
  321 + deviceProfileIds: deviceProfileId ? [deviceProfileId] : null,
344 }, 322 },
345 - }); 323 + };
  324 + delete obj.deviceProfileId;
  325 + return obj;
  326 + },
  327 + formConfig: {
  328 + labelWidth: 100,
  329 + schemas: searchFormSchema,
  330 + resetFunc: resetFn,
  331 + },
  332 + useSearchForm: true,
  333 + showTableSetting: true,
  334 + bordered: true,
  335 + showIndexColumn: false,
  336 + rowKey: 'id',
  337 + searchInfo: searchInfo,
  338 + clickToRowSelect: false,
  339 + actionColumn: {
  340 + width: 200,
  341 + title: '操作',
  342 + slots: { customRender: 'action' },
  343 + fixed: 'right',
  344 + },
  345 + rowSelection: {
  346 + type: 'checkbox',
  347 + getCheckboxProps: (record: DeviceModel) => {
  348 + return { disabled: !!record.customerId };
  349 + },
  350 + onSelect(_record, _selected, selectedRows) {
  351 + const [firstItem] = selectedRows as DeviceRecord[];
  352 + const { deviceType } = firstItem || {};
  353 + batchUpdateProductFlag.value =
  354 + !selectedRows.length ||
  355 + !selectedRows.every((item) => (item as DeviceRecord).deviceType === deviceType);
  356 + },
  357 + onSelectAll(_selected, selectedRows) {
  358 + const [firstItem] = selectedRows as DeviceRecord[];
  359 + const { deviceType } = firstItem || {};
  360 + batchUpdateProductFlag.value =
  361 + !selectedRows.length ||
  362 + !selectedRows.every((item) => (item as DeviceRecord).deviceType === deviceType);
  363 + },
  364 + },
  365 + });
346 366
347 - const { isExistOption } = useBatchOperation(getRowSelection, setSelectedRowKeys); 367 + const { isExistOption } = useBatchOperation(getRowSelection, setSelectedRowKeys);
348 368
349 - const userInfo: any = getAuthCache(USER_INFO_KEY);  
350 - const role: string = userInfo.roles[0]; 369 + const userInfo: any = getAuthCache(USER_INFO_KEY);
  370 + const role: string = userInfo.roles[0];
351 371
352 - function handleCreate() {  
353 - openModal(true, {  
354 - isUpdate: false,  
355 - });  
356 - }  
357 - // 分配客户  
358 - function handleDispatchCustomer(record: Recordable) {  
359 - openCustomerModal(true, record);  
360 - }  
361 - function handleReload() {  
362 - setSelectedRowKeys([]);  
363 - handleSuccess();  
364 - }  
365 - // 取消分配客户  
366 - async function handleCancelDispatchCustomer(record: Recordable) {  
367 - try {  
368 - // 该设备是否正在被场景联动使用中?  
369 - await cancelDispatchCustomer(record);  
370 - handleReload();  
371 - } catch {}  
372 - } 372 + function handleCreate() {
  373 + openModal(true, {
  374 + isUpdate: false,
  375 + });
  376 + }
  377 + // 分配客户
  378 + function handleDispatchCustomer(record: Recordable) {
  379 + openCustomerModal(true, record);
  380 + }
  381 + function handleReload() {
  382 + setSelectedRowKeys([]);
  383 + handleSuccess();
  384 + }
  385 + // 取消分配客户
  386 + async function handleCancelDispatchCustomer(record: Recordable) {
  387 + try {
  388 + // 该设备是否正在被场景联动使用中?
  389 + await cancelDispatchCustomer(record);
  390 + handleReload();
  391 + } catch {}
  392 + }
373 393
374 - function handleDetail(record: Recordable) {  
375 - const { id, tbDeviceId, deviceProfile, deviceType } = record;  
376 - const { transportType } = deviceProfile || {};  
377 - openDrawer(true, {  
378 - id,  
379 - tbDeviceId,  
380 - transportType,  
381 - deviceType,  
382 - });  
383 - } 394 + function handleDetail(record: Recordable) {
  395 + const { id, tbDeviceId, deviceProfile, deviceType } = record;
  396 + const { transportType } = deviceProfile || {};
  397 + openDrawer(true, {
  398 + id,
  399 + tbDeviceId,
  400 + transportType,
  401 + deviceType,
  402 + });
  403 + }
384 404
385 - async function handleEdit(record: Recordable) {  
386 - if (record.deviceType === 'SENSOR') {  
387 - const res = await getGATEWAY(record.tbDeviceId);  
388 - Reflect.set(record, 'gatewayId', res.tbDeviceId);  
389 - }  
390 - openModal(true, {  
391 - isUpdate: true,  
392 - record,  
393 - });  
394 - }  
395 - function handleSuccess() {  
396 - reload();  
397 - }  
398 - function handleSelect(organization) {  
399 - searchInfo.organizationId = organization;  
400 - handleSuccess();  
401 - }  
402 - function goDeviceProfile(e) {  
403 - go(PageEnum.DEVICE_PROFILE + '?name=' + encodeURIComponent(String(e)));  
404 - }  
405 - const { copied, copy } = useClipboard({ legacy: true });  
406 - const copySN = async (snCode: string) => {  
407 - await copy(snCode);  
408 - if (unref(copied)) {  
409 - createMessage.success('复制成功~');  
410 - }  
411 - }; 405 + async function handleEdit(record: Recordable) {
  406 + if (record.deviceType === 'SENSOR') {
  407 + const res = await getGATEWAY(record.tbDeviceId);
  408 + Reflect.set(record, 'gatewayId', res.tbDeviceId);
  409 + }
  410 + openModal(true, {
  411 + isUpdate: true,
  412 + record,
  413 + });
  414 + }
  415 + function handleSuccess() {
  416 + reload();
  417 + }
  418 + function handleSelect(organization) {
  419 + searchInfo.organizationId = organization;
  420 + handleSuccess();
  421 + }
  422 + function goDeviceProfile(e) {
  423 + go(PageEnum.DEVICE_PROFILE + '?name=' + encodeURIComponent(String(e)));
  424 + }
412 425
413 - const handleOpenTbDeviceDetail = (data: { id: string; tbDeviceId: string }) => {  
414 - openTbDeviceDrawer(true, data);  
415 - }; 426 + const handleOpenTbDeviceDetail = (data: { id: string; tbDeviceId: string }) => {
  427 + openTbDeviceDrawer(true, data);
  428 + };
416 429
417 - const handleOpenGatewayDetail = (data: { id: string; tbDeviceId: string }) => {  
418 - openGatewayDetailDrawer(true, data);  
419 - }; 430 + const handleOpenGatewayDetail = (data: { id: string; tbDeviceId: string }) => {
  431 + openGatewayDetailDrawer(true, data);
  432 + };
420 433
421 - const handleUpAndDownRecord = (record: Record<'name' | 'alias', string>) => {  
422 - ROUTER.push({  
423 - path: '/operation/onlinerecord',  
424 - query: { deviceName: record.alias || record.name },  
425 - });  
426 - }; 434 + const handleUpAndDownRecord = (record: Record<'name' | 'alias', string>) => {
  435 + ROUTER.push({
  436 + path: '/operation/onlinerecord',
  437 + query: { deviceName: record.alias || record.name },
  438 + });
  439 + };
427 440
428 - const handleCheckHasDiffenterOrg = (options: DeviceModel[]) => {  
429 - let orgId: string | undefined;  
430 - let flag = false;  
431 - for (const item of options) {  
432 - const _orgId = item.organizationId;  
433 - if (!orgId) orgId = _orgId;  
434 - if (orgId !== _orgId) {  
435 - flag = true;  
436 - break;  
437 - }  
438 - }  
439 - return flag;  
440 - }; 441 + const handleCheckHasDiffenterOrg = (options: DeviceModel[]) => {
  442 + let orgId: string | undefined;
  443 + let flag = false;
  444 + for (const item of options) {
  445 + const _orgId = item.organizationId;
  446 + if (!orgId) orgId = _orgId;
  447 + if (orgId !== _orgId) {
  448 + flag = true;
  449 + break;
  450 + }
  451 + }
  452 + return flag;
  453 + };
441 454
442 - const handleBatchAssign = () => {  
443 - const options = getSelectRows();  
444 - if (handleCheckHasDiffenterOrg(options as DeviceModel[])) {  
445 - createMessage.error('当前选中项中存在不同所属组织的设备!');  
446 - return;  
447 - }  
448 - openCustomerModal(true, options);  
449 - }; 455 + const handleBatchAssign = () => {
  456 + const options = getSelectRows();
  457 + if (handleCheckHasDiffenterOrg(options as DeviceModel[])) {
  458 + createMessage.error('当前选中项中存在不同所属组织的设备!');
  459 + return;
  460 + }
  461 + openCustomerModal(true, options);
  462 + };
450 463
451 - const handleDelete = async (record?: DeviceRecord) => {  
452 - let ids: string[] = [];  
453 - if (record) {  
454 - ids.push(record.id);  
455 - } else {  
456 - ids = getSelectRowKeys();  
457 - }  
458 - try {  
459 - setLoading(true);  
460 - await deleteDevice(ids);  
461 - createMessage.success('删除成功');  
462 - handleReload();  
463 - } catch (error) {  
464 - throw error;  
465 - } finally {  
466 - setLoading(false);  
467 - }  
468 - }; 464 + const handleDelete = async (record?: DeviceRecord) => {
  465 + let ids: string[] = [];
  466 + if (record) {
  467 + ids.push(record.id);
  468 + } else {
  469 + ids = getSelectRowKeys();
  470 + }
  471 + try {
  472 + setLoading(true);
  473 + await deleteDevice(ids);
  474 + createMessage.success('删除成功');
  475 + handleReload();
  476 + } catch (error) {
  477 + throw error;
  478 + } finally {
  479 + setLoading(false);
  480 + }
  481 + };
469 482
470 - const handleBatchImport = () => {  
471 - openImportModal(true);  
472 - }; 483 + const handleBatchImport = () => {
  484 + openImportModal(true);
  485 + };
473 486
474 - const handleImportFinally = () => {  
475 - reload();  
476 - }; 487 + const handleImportFinally = () => {
  488 + reload();
  489 + };
477 490
478 - const { createSyncConfirm } = useSyncConfirm();  
479 - const handlePublicDevice = async (record: DeviceRecord) => {  
480 - try {  
481 - const publicFlag = record?.customerAdditionalInfo?.isPublic;  
482 - const type = publicFlag ? '私有' : '公开';  
483 - const flag = await createSyncConfirm({  
484 - iconType: 'warning',  
485 - title: `您确定要将设备 '${record.name}' 设为${type}吗?`,  
486 - content: `确认后,设备及其所有数据将被设为${type}并${  
487 - publicFlag ? '不' : ''  
488 - }可被其他人访问。`,  
489 - });  
490 - if (!flag) return;  
491 - if (publicFlag) {  
492 - await privateDevice(record.tbDeviceId);  
493 - } else {  
494 - await publicDevice(record.tbDeviceId);  
495 - }  
496 - reload();  
497 - } catch (error) {}  
498 - }; 491 + const { createSyncConfirm } = useSyncConfirm();
  492 + const handlePublicDevice = async (record: DeviceRecord) => {
  493 + try {
  494 + const publicFlag = record?.customerAdditionalInfo?.isPublic;
  495 + const type = publicFlag ? '私有' : '公开';
  496 + const flag = await createSyncConfirm({
  497 + iconType: 'warning',
  498 + title: `您确定要将设备 '${record.name}' 设为${type}吗?`,
  499 + content: `确认后,设备及其所有数据将被设为${type}并${
  500 + publicFlag ? '不' : ''
  501 + }可被其他人访问。`,
  502 + });
  503 + if (!flag) return;
  504 + if (publicFlag) {
  505 + await privateDevice(record.tbDeviceId);
  506 + } else {
  507 + await publicDevice(record.tbDeviceId);
  508 + }
  509 + reload();
  510 + } catch (error) {}
  511 + };
499 512
500 - // 收藏 && 批量收藏  
501 - const handelCollect = async (record?: Recordable) => {  
502 - let ids: string[] = [];  
503 - if (record) {  
504 - ids.push(record.id);  
505 - } else {  
506 - ids = await getSelectRowKeys();  
507 - }  
508 - try {  
509 - setLoading(true);  
510 - await deviceCollect(ids);  
511 - createMessage.success('操作成功');  
512 - handleReload();  
513 - } catch (error) {  
514 - throw error;  
515 - } finally {  
516 - setLoading(false);  
517 - }  
518 - }; 513 + // 收藏 && 批量收藏
  514 + const handelCollect = async (record?: Recordable) => {
  515 + let ids: string[] = [];
  516 + if (record) {
  517 + ids.push(record.id);
  518 + } else {
  519 + ids = await getSelectRowKeys();
  520 + }
  521 + try {
  522 + setLoading(true);
  523 + await deviceCollect(ids);
  524 + createMessage.success('操作成功');
  525 + handleReload();
  526 + } catch (error) {
  527 + throw error;
  528 + } finally {
  529 + setLoading(false);
  530 + }
  531 + };
519 532
520 - onMounted(() => {  
521 - const queryParams = ROUTE.query as Record<'deviceProfileId', undefined | string>;  
522 - const { setFieldsValue } = getForm();  
523 - setFieldsValue({ deviceProfileId: queryParams.deviceProfileId });  
524 - });  
525 - return {  
526 - registerTable,  
527 - handleCreate,  
528 - handleDetail,  
529 - handleEdit,  
530 - handleSuccess,  
531 - goDeviceProfile,  
532 - handleSelect,  
533 - registerModal,  
534 - registerDetailDrawer,  
535 - DeviceTypeEnum,  
536 - DeviceState,  
537 - searchInfo,  
538 - organizationIdTreeRef,  
539 - handleDispatchCustomer,  
540 - handleCancelDispatchCustomer,  
541 - registerCustomerModal,  
542 - authBtn,  
543 - role,  
544 - copySN,  
545 - isExistOption,  
546 - handleDelete,  
547 - handelCollect,  
548 - // hasBatchDelete,  
549 - // handleDeleteOrBatchDelete,  
550 - handleReload,  
551 - registerTbDetailDrawer,  
552 - handleOpenTbDeviceDetail,  
553 - handleOpenGatewayDetail,  
554 - registerGatewayDetailDrawer,  
555 - handleUpAndDownRecord,  
556 - handleBatchAssign,  
557 - registerImportModal,  
558 - handleBatchImport,  
559 - handleImportFinally,  
560 - handlePublicDevice,  
561 - isCustomer,  
562 - };  
563 - }, 533 + onMounted(() => {
  534 + const queryParams = ROUTE.query as Record<'deviceProfileId', undefined | string>;
  535 + const { setFieldsValue } = getForm();
  536 + setFieldsValue({ deviceProfileId: queryParams.deviceProfileId });
564 }); 537 });
  538 +
  539 + const handelOpenBatchUpdateProductModal = () => {
  540 + const rows: DeviceRecord[] = getSelectRows();
  541 + const [firstItem] = rows;
  542 +
  543 + openBatchUpdateProductModal(true, {
  544 + mode: DataActionModeEnum.UPDATE,
  545 + record: {
  546 + sourceDeviceProfileName: firstItem.deviceProfile.name,
  547 + deviceType: firstItem.deviceType,
  548 + deviceIds: rows.map((item) => item.tbDeviceId),
  549 + },
  550 + } as BatchUpdateProductModalParamsType);
  551 + };
  552 +
  553 + const handleBatchUpdateProductSuccess = () => {
  554 + reload();
  555 + clearSelectedRowKeys();
  556 + batchUpdateProductFlag.value = true;
  557 + };
565 </script> 558 </script>
566 559
567 -<style scoped lang="css">  
568 - /* /deep/.ant-message-notice-content {  
569 - max-width: 600px !important;  
570 - } */  
571 -</style> 560 +<style scoped lang="css"></style>