wzyyy 3 anni fa
parent
commit
d3540728ba
25 ha cambiato i file con 666 aggiunte e 152 eliminazioni
  1. 2 2
      src/components/Metadata/EditTable/index.tsx
  2. 64 12
      src/components/Metadata/JsonParam/index.tsx
  3. 50 6
      src/components/ProTableCard/CardItems/device.tsx
  4. 54 4
      src/components/ProTableCard/CardItems/product.tsx
  5. 5 2
      src/components/ProTableCard/TableCard.tsx
  6. 7 2
      src/components/ProTableCard/index.tsx
  7. 1 1
      src/pages/DataCollect/Collector/components/Point/Save/BatchUpdate.tsx
  8. 30 3
      src/pages/DataCollect/Collector/components/Point/Save/modbus.tsx
  9. 1 1
      src/pages/DataCollect/Collector/components/Point/Save/opc-ua.tsx
  10. 5 0
      src/pages/DataCollect/service.ts
  11. 1 1
      src/pages/device/Category/Save/index.tsx
  12. 2 0
      src/pages/device/Instance/Detail/Running/Property/PropertyCard.less
  13. 11 10
      src/pages/device/components/Metadata/Base/Edit/index.tsx
  14. 1 1
      src/pages/device/components/Metadata/index.less
  15. 2 1
      src/pages/device/components/Metadata/index.tsx
  16. 7 5
      src/pages/edge/Resource/index.tsx
  17. 1 1
      src/pages/link/Channel/Modbus/savePoint.tsx
  18. 135 20
      src/pages/system/Department/Assets/deivce/bind.tsx
  19. 55 24
      src/pages/system/Department/Assets/deivce/index.tsx
  20. 11 0
      src/pages/system/Department/Assets/index.less
  21. 135 29
      src/pages/system/Department/Assets/product/bind.tsx
  22. 70 24
      src/pages/system/Department/Assets/product/index.tsx
  23. 6 0
      src/pages/system/Department/Assets/service.ts
  24. 6 1
      src/pages/system/Menu/Detail/buttons.tsx
  25. 4 2
      src/pages/system/User/Save/index.tsx

+ 2 - 2
src/components/Metadata/EditTable/index.tsx

@@ -168,8 +168,8 @@ Editable.Popover = observer((props) => {
         </div>
         <CloseOutlined
           onClick={() => {
-            // setVisible(false);
-            closePopover();
+            setVisible(false);
+            // closePopover();
           }}
         />
       </div>

+ 64 - 12
src/components/Metadata/JsonParam/index.tsx

@@ -45,16 +45,66 @@ const JsonParam = observer((props: Props) => {
       return _data;
     });
 
-  const checkArray: any = (arr: any) => {
+  // const checkArray: any = (arr: any) => {
+  //   if (Array.isArray(arr) && arr.length) {
+  //     return arr.every((item: any) => {
+  //       if (item.valueType?.type === 'object') {
+  //         return item.id && item.name && checkArray(item.json?.properties);
+  //       }
+  //       return item.id && item.name && item.valueType;
+  //     });
+  //   }
+  //   return false;
+  // };
+
+  const checkArrayFormat: any = (arr: any) => {
+    const reg = new RegExp('^[0-9a-zA-Z_\\\\-]+$');
+    let str: string = '';
     if (Array.isArray(arr) && arr.length) {
-      return arr.every((item: any) => {
-        if (item.valueType?.type === 'object') {
-          return item.id && item.name && checkArray(item.json?.properties);
+      arr.every((item: any) => {
+        if (!item.id) {
+          str = '请输入标识';
+          return false;
+        }
+        if (!item.name) {
+          str = '请输入名称';
+          return false;
+        }
+        if (!item.valueType?.type) {
+          str = '请选择数据类型';
+          return false;
+        }
+        if (!reg.exec(item.id)) {
+          str = '标识只能由数字、字母、下划线、中划线组成';
+          return false;
+        }
+        if (item.id.length > 64 && item.name.length > 64) {
+          str = '标识最多可输入64个字符';
+          return false;
+        }
+        if (item.name.length > 64) {
+          str = '名称最多可输入64个字符';
+          return false;
+        }
+        if (item.valueType?.type === 'boolean') {
+          if (!(item.valueType.falseText && item.valueType.trueText)) {
+            str = '请输入布尔值';
+            return false;
+          }
+          if (
+            item.valueType.falseValue === '' ||
+            item.valueType.trueValue === '' ||
+            item.valueType.falseValue === undefined ||
+            item.valueType.trueValue === undefined
+          ) {
+            str = '请输入布尔值';
+            return false;
+          }
         }
-        return item.id && item.name && item.valueType;
+        return item.id && item.name && item.valueType?.type;
       });
     }
-    return false;
+    return str;
   };
 
   const schema: ISchema = {
@@ -80,12 +130,8 @@ const JsonParam = observer((props: Props) => {
                 if (props.keys === 'inputs' && value.length === 0) {
                   resolve('');
                 }
-                const flag = checkArray(value);
-                if (!!flag) {
-                  resolve('');
-                } else {
-                  resolve('请配置参数');
-                }
+                const str = checkArrayFormat(value);
+                resolve(str);
               });
             },
           },
@@ -162,6 +208,12 @@ const JsonParam = observer((props: Props) => {
                           item.value,
                         ),
                       ),
+                      'x-validator': [
+                        {
+                          required: true,
+                          message: '请选择数据类型',
+                        },
+                      ],
                     },
                     booleanConfig: {
                       title: '布尔值',

+ 50 - 6
src/components/ProTableCard/CardItems/device.tsx

@@ -1,10 +1,12 @@
-import React, { useState } from 'react';
+import React, { useEffect, useRef, useState } from 'react';
 import type { DeviceInstance } from '@/pages/device/Instance/typings';
 import { StatusColorEnum } from '@/components/BadgeStatus';
 import { TableCard, Ellipsis } from '@/components';
 import '@/style/common.less';
 import '../index.less';
 import { CheckOutlined } from '@ant-design/icons';
+import { Store } from 'jetlinks-store';
+import { Checkbox } from 'antd';
 
 export interface DeviceCardProps extends Partial<DeviceInstance> {
   detail?: React.ReactNode;
@@ -18,6 +20,11 @@ export interface DeviceCardProps extends Partial<DeviceInstance> {
   showBindBtn?: boolean;
   cardType?: 'bind' | 'unbind';
   showTool?: boolean;
+  assetsOptions?: any[];
+  permissionInfoList?: any[];
+  allAssets?: string[];
+  onAssetsChange?: (value?: any) => void;
+  isChecked?: boolean;
 }
 
 const defaultImage = require('/public/images/device-type-3-big.png');
@@ -36,6 +43,32 @@ export const handlePermissionsMap = (permissions?: string[]) => {
 
 export const ExtraDeviceCard = (props: DeviceCardProps) => {
   const [imgUrl, setImgUrl] = useState<string>(props.photoUrl || defaultImage);
+  const [assetKeys, setAssetKeys] = useState<string[]>(['read']);
+  const [disabled, setDisabled] = useState(true);
+  const disabledRef = useRef<boolean>(true);
+
+  const assetsOptions =
+    props.assetsOptions && props.permissionInfoList
+      ? props.assetsOptions.filter((item: any) =>
+          props.permissionInfoList.some((pItem) => pItem.id === item.value),
+        )
+      : [];
+
+  useEffect(() => {
+    Store.subscribe('assets-device', (data: any) => {
+      if (data.isAll && data.bindKeys.includes(props.id)) {
+        setAssetKeys(data.assets);
+        setDisabled(true);
+        disabledRef.current = true;
+      } else if (!data.isAll && data.bindKeys.includes(props.id)) {
+        setDisabled(false);
+        disabledRef.current = false;
+      } else {
+        setDisabled(true);
+        disabledRef.current = true;
+      }
+    });
+  }, []);
 
   return (
     <TableCard
@@ -78,11 +111,22 @@ export const ExtraDeviceCard = (props: DeviceCardProps) => {
             </div>
             {props.cardType === 'bind' ? (
               <div className={'flex-auto'}>
-                <label>说明</label>
-                <Ellipsis title={props.describe || ''} />
-                {/*<Tooltip title={props.describe}>*/}
-                {/*  <div className={'ellipsis'}>{props.describe}</div>*/}
-                {/*</Tooltip>*/}
+                <Checkbox.Group
+                  options={assetsOptions?.map((item) => {
+                    return {
+                      ...item,
+                      disabled: disabled,
+                    };
+                  })}
+                  value={assetKeys}
+                  onChange={(e) => {
+                    console.log('assetKeys', disabledRef.current, assetKeys, e);
+                    if (!disabledRef.current) {
+                      setAssetKeys(e as string[]);
+                      props.onAssetsChange?.(e);
+                    }
+                  }}
+                />
               </div>
             ) : (
               <div className={'flex-auto'}>

+ 54 - 4
src/components/ProTableCard/CardItems/product.tsx

@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React, { useEffect, useRef, useState } from 'react';
 import type { ProductItem } from '@/pages/device/Product/typings';
 import { StatusColorEnum } from '@/components/BadgeStatus';
 import { useIntl } from 'umi';
@@ -6,6 +6,8 @@ import { Ellipsis, TableCard } from '@/components';
 import '@/style/common.less';
 import '../index.less';
 import { CheckOutlined } from '@ant-design/icons';
+import { Checkbox } from 'antd';
+import { Store } from 'jetlinks-store';
 
 export interface ProductCardProps extends Partial<ProductItem> {
   detail?: React.ReactNode;
@@ -19,6 +21,11 @@ export interface ProductCardProps extends Partial<ProductItem> {
   showBindBtn?: boolean;
   cardType?: 'bind' | 'unbind';
   showTool?: boolean;
+  assetsOptions?: any[];
+  permissionInfoList?: any[];
+  allAssets?: string[];
+  onAssetsChange?: (value?: any) => void;
+  isChecked?: boolean;
 }
 
 const defaultImage = require('/public/images/device-product.png');
@@ -99,6 +106,32 @@ export const ExtraSceneProductCard = (props: ProductCardProps) => {
 export const ExtraProductCard = (props: ProductCardProps) => {
   const intl = useIntl();
   const [imgUrl, setImgUrl] = useState<string>(props.photoUrl || defaultImage);
+  const [assetKeys, setAssetKeys] = useState<string[]>(['read']);
+  const [disabled, setDisabled] = useState(true);
+  const disabledRef = useRef<boolean>(true);
+  console.log(props.assetsOptions, props.permissionInfoList);
+  const assetsOptions =
+    props.assetsOptions && props.permissionInfoList
+      ? props.assetsOptions.filter((item: any) =>
+          props.permissionInfoList.some((pItem) => pItem.id === item.value),
+        )
+      : [];
+
+  useEffect(() => {
+    Store.subscribe('assets-product', (data: any) => {
+      if (data.isAll && data.bindKeys.includes(props.id)) {
+        setAssetKeys(data.assets);
+        setDisabled(true);
+        disabledRef.current = true;
+      } else if (!data.isAll && data.bindKeys.includes(props.id)) {
+        setDisabled(false);
+        disabledRef.current = false;
+      } else {
+        setDisabled(true);
+        disabledRef.current = true;
+      }
+    });
+  }, []);
 
   return (
     <TableCard
@@ -115,7 +148,10 @@ export const ExtraProductCard = (props: ProductCardProps) => {
         1: StatusColorEnum.success,
       }}
       className={props.className}
-      onClick={props.onClick}
+      onClick={(e) => {
+        e.stopPropagation();
+        props.onClick?.(e);
+      }}
     >
       <div className={'pro-table-card-item'}>
         <div className={'card-item-avatar'}>
@@ -143,8 +179,22 @@ export const ExtraProductCard = (props: ProductCardProps) => {
             </div>
             {props.cardType === 'bind' ? (
               <div className={'flex-auto'}>
-                <label>说明</label>
-                <Ellipsis title={props.describe} />
+                <Checkbox.Group
+                  options={assetsOptions?.map((item) => {
+                    return {
+                      ...item,
+                      disabled: item.disabled !== true ? disabled : item.disabled,
+                    };
+                  })}
+                  value={assetKeys}
+                  onChange={(e) => {
+                    console.log('assetKeys', disabledRef.current, assetKeys, e);
+                    if (!disabledRef.current) {
+                      setAssetKeys(e as string[]);
+                      props.onAssetsChange?.(e);
+                    }
+                  }}
+                />
               </div>
             ) : (
               <div className={'flex-auto'}>

+ 5 - 2
src/components/ProTableCard/TableCard.tsx

@@ -17,7 +17,7 @@ export interface TableCardProps {
   children?: React.ReactNode;
   actions?: React.ReactNode[];
   contentClassName?: string;
-  onClick?: () => void;
+  onClick?: React.MouseEventHandler<HTMLDivElement>;
 }
 
 function getAction(actions: React.ReactNode[]) {
@@ -76,7 +76,10 @@ export default (props: TableCardProps) => {
   return (
     <div
       className={classNames('iot-card', { hover: maskShow }, props.className)}
-      onClick={props.onClick}
+      onClick={(e) => {
+        e.stopPropagation();
+        props.onClick?.(e);
+      }}
     >
       <div className={'card-warp'}>
         <div

+ 7 - 2
src/components/ProTableCard/index.tsx

@@ -37,6 +37,7 @@ interface ProTableCardProps<T> {
   noPadding?: boolean;
   cardScrollY?: number;
   modelChange?: (type: ModelType) => void;
+  cardProps?: any;
 }
 
 const ProTableCard = <
@@ -71,10 +72,12 @@ const ProTableCard = <
    * @param dataSource
    */
   const handleCard = useCallback(
-    (dataSource: readonly T[] | undefined, rowSelection?: any): JSX.Element => {
+    (dataSource: readonly T[] | undefined, rowSelection?: any, _cardProps?: any): JSX.Element => {
       setDataLength(dataSource ? dataSource.length : 0);
 
       const Item = (dom: React.ReactNode) => {
+        // @ts-ignore
+        const card_props = isFunction(_cardProps) ? _cardProps(dom.props) : _cardProps;
         if (!rowSelection || (rowSelection && !rowSelection.selectedRowKeys)) {
           return dom;
         }
@@ -85,12 +88,14 @@ const ProTableCard = <
 
         // @ts-ignore
         return React.cloneElement(dom, {
+          ...card_props,
           // @ts-ignore
           className: classNames(dom.props.className, {
             'item-active': selectedRowKeys && selectedRowKeys.includes(id),
           }),
           key: id,
           onClick: (e: any) => {
+            if (e.target.nodeName !== 'DIV') return;
             e.stopPropagation();
             if (onChange || onSelect) {
               const isSelect = selectedRowKeys.includes(id);
@@ -299,7 +304,7 @@ const ProTableCard = <
         tableViewRender={
           model === ModelEnum.CARD
             ? (tableProps) => {
-                return handleCard(tableProps.dataSource, extraProps?.rowSelection);
+                return handleCard(tableProps.dataSource, extraProps?.rowSelection, props.cardProps);
               }
             : undefined
         }

+ 1 - 1
src/pages/DataCollect/Collector/components/Point/Save/BatchUpdate.tsx

@@ -40,7 +40,7 @@ export default (props: Props) => {
       if (!(Number(value) % 1 === 0) || Number(value) <= 0) {
         return {
           type: 'error',
-          message: '请输入非0正整数',
+          message: '请输入正整数',
         };
       }
       return '';

+ 30 - 3
src/pages/DataCollect/Collector/components/Point/Save/modbus.tsx

@@ -103,7 +103,7 @@ export default (props: Props) => {
       if (!(Number(value) % 1 === 0) || Number(value) < 0) {
         return {
           type: 'error',
-          message: '请输入0或正整数',
+          message: '请输入正整数',
         };
       }
       return '';
@@ -209,6 +209,33 @@ export default (props: Props) => {
               {
                 checkAddressLength: true,
               },
+              {
+                triggerType: 'onBlur',
+                validator: (value: string) => {
+                  return new Promise((resolve) => {
+                    if (props.data?.id && props.data?.configuration?.parameter?.address === value) {
+                      resolve('');
+                    }
+                    service
+                      ._validateField(props.collector?.id || '', {
+                        pointKey: value,
+                      })
+                      .then((resp) => {
+                        if (resp.status === 200) {
+                          if (resp.result.passed) {
+                            resolve('');
+                          } else {
+                            resolve('该地址已存在');
+                          }
+                        }
+                        resolve('');
+                      })
+                      .catch(() => {
+                        return '验证失败!';
+                      });
+                  });
+                },
+              },
             ],
           },
           'configuration.parameter.quantity': {
@@ -240,7 +267,7 @@ export default (props: Props) => {
               },
               {
                 min: 1,
-                message: '请输入非0正整数',
+                message: '请输入正整数',
               },
               {
                 checkLength: true,
@@ -461,7 +488,7 @@ export default (props: Props) => {
               },
               {
                 min: 1,
-                message: '请输入非0正整数',
+                message: '请输入正整数',
               },
               {
                 checkLength: true,

+ 1 - 1
src/pages/DataCollect/Collector/components/Point/Save/opc-ua.tsx

@@ -89,7 +89,7 @@ export default (props: Props) => {
       if (!(Number(value) % 1 === 0) || Number(value) < 0) {
         return {
           type: 'error',
-          message: '请输入0或正整数',
+          message: '请输入正整数',
         };
       }
       return '';

+ 5 - 0
src/pages/DataCollect/service.ts

@@ -169,6 +169,11 @@ class Service {
       method: 'POST',
       data: params,
     });
+  public _validateField = (id: string, data?: any) =>
+    request(`/${SystemConst.API_BASE}/data-collect/point/${id}/_validate`, {
+      method: 'GET',
+      params: data,
+    });
 }
 
 const service = new Service();

+ 1 - 1
src/pages/device/Category/Save/index.tsx

@@ -136,7 +136,7 @@ const Save = (props: Props) => {
         'x-validator': [
           {
             format: 'integer',
-            message: '请输入非0正整数',
+            message: '请输入正整数',
           },
         ],
       },

+ 2 - 0
src/pages/device/Instance/Detail/Running/Property/PropertyCard.less

@@ -6,8 +6,10 @@
 
 .card-title-box {
   display: flex;
+  align-items: center;
   justify-content: space-between;
   width: 100%;
+  height: 32px;
   margin-bottom: 10px;
 
   .card-title {

+ 11 - 10
src/pages/device/components/Metadata/Base/Edit/index.tsx

@@ -797,16 +797,17 @@ const Edit = observer((props: Props) => {
                     },
                     'x-validator': [
                       {
-                        required: true,
-                        message: `请输入步长`,
-                      },
-                      {
-                        format: 'integer',
-                        message: '请输入正整数',
-                      },
-                      {
-                        min: 1,
-                        message: '请输入正整数',
+                        // triggerType: 'onBlur',
+                        validator: (value: any[]) => {
+                          return new Promise((resolve) => {
+                            const number = Number(value);
+                            if (number <= 0 || value.length > 64 || /[.]/.test(value)) {
+                              resolve('请输入非0正整数,最多可输入64个字符');
+                            } else {
+                              resolve('');
+                            }
+                          });
+                        },
                       },
                     ],
                     'x-reactions': [

+ 1 - 1
src/pages/device/components/Metadata/index.less

@@ -11,6 +11,6 @@
   position: absolute;
   top: 12px;
   z-index: 1;
-  margin-left: 330px;
+  margin-left: 380px;
   font-weight: 100;
 }

+ 2 - 1
src/pages/device/components/Metadata/index.tsx

@@ -50,7 +50,7 @@ const Metadata = observer((props: Props) => {
 
   return (
     <div className={'device-detail-metadata'} style={{ position: 'relative' }}>
-      <div className={styles.tips} style={{ width: '40%' }}>
+      <div className={styles.tips} style={{ width: 'calc(100% - 670px)' }}>
         <Tooltip
           title={
             InstanceModel.detail?.independentMetadata && props.type === 'device'
@@ -68,6 +68,7 @@ const Metadata = observer((props: Props) => {
       </div>
       <Tabs
         className={styles.metadataNav}
+        type="card"
         tabBarExtraContent={
           <Space>
             {InstanceModel.detail?.independentMetadata && props.type === 'device' && (

+ 7 - 5
src/pages/edge/Resource/index.tsx

@@ -69,7 +69,9 @@ export default () => {
       style={{ padding: 0 }}
       popConfirm={{
         title: intl.formatMessage({
-          id: `pages.data.option.${record.state?.value}.tips`,
+          id: `pages.data.option.${
+            record.state.value !== 'disabled' ? 'disabled' : 'enabled'
+          }.tips`,
           defaultMessage: '确认禁用?',
         }),
         onConfirm: () => {
@@ -119,15 +121,15 @@ export default () => {
       key={'delete'}
       style={{ padding: 0 }}
       isPermission={permission.delete}
-      tooltip={record.state.value !== 'notActive' ? { title: '请先禁用,再删除。' } : undefined}
-      disabled={record.state.value !== 'notActive'}
+      tooltip={record.state.value !== 'disabled' ? { title: '请先禁用,再删除。' } : undefined}
+      disabled={record.state.value !== 'disabled'}
       popConfirm={{
         title: intl.formatMessage({
           id: 'pages.data.option.remove.tips',
         }),
-        disabled: record.state.value !== 'notActive',
+        disabled: record.state.value !== 'disabled',
         onConfirm: async () => {
-          if (record.state.value === 'notActive') {
+          if (record.state.value === 'disabled') {
             await service.remove(record.id);
             onlyMessage(
               intl.formatMessage({

+ 1 - 1
src/pages/link/Channel/Modbus/savePoint.tsx

@@ -102,7 +102,7 @@ const SavePoint = (props: Props) => {
                     if (value !== 0 || /(^[1-9]\d*$)/.test(value)) {
                       return Promise.resolve();
                     }
-                    return Promise.reject(new Error('请输入非0正整数'));
+                    return Promise.reject(new Error('请输入正整数'));
                   },
                 }),
               ]}

+ 135 - 20
src/pages/system/Department/Assets/deivce/bind.tsx

@@ -1,31 +1,37 @@
 // 资产-产品分类-绑定
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
 import { DeviceBadge, service } from './index';
-import { message, Modal } from 'antd';
+import { Checkbox, message, Modal, Switch } from 'antd';
 import Models from './model';
 import { useEffect, useRef, useState } from 'react';
 import { observer } from '@formily/react';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import type { DeviceItem } from '@/pages/system/Department/typings';
-import PermissionModal from '@/pages/system/Department/Assets/permissionModal';
 import SearchComponent from '@/components/SearchComponent';
 import { ExtraDeviceCard } from '@/components/ProTableCard/CardItems/device';
 import { ProTableCard } from '@/components';
 import { AssetsModel } from '@/pages/system/Department/Assets';
 import encodeQuery from '@/utils/encodeQuery';
+import { ExclamationCircleOutlined } from '@ant-design/icons';
+import { Store } from 'jetlinks-store';
+import { onlyMessage } from '@/utils/util';
 
 interface Props {
   reload: () => void;
   visible: boolean;
   onCancel: () => void;
   parentId: string;
+  assetsType?: string[];
 }
 
 const Bind = observer((props: Props) => {
   const intl = useIntl();
   const actionRef = useRef<ActionType>();
   const [searchParam, setSearchParam] = useState({});
-  const saveRef = useRef<{ saveData: Function }>();
+  const [loading, setLoading] = useState(false);
+  const [checkAssets, setCheckAssets] = useState<string[]>(['read']);
+  const [isAll, setIsAll] = useState<boolean>(false);
+  const bindChecks = useRef(new Map());
 
   const columns: ProColumns<DeviceItem>[] = [
     {
@@ -108,7 +114,26 @@ const Bind = observer((props: Props) => {
   const handleBind = () => {
     AssetsModel.params = {};
     if (Models.bindKeys.length) {
-      saveRef.current?.saveData();
+      setLoading(true);
+      const _data: any[] = [];
+      bindChecks.current.forEach((value, key) => {
+        _data.push({
+          targetType: 'org',
+          targetId: props.parentId,
+          assetType: 'device',
+          assetIdList: [key],
+          permission: value,
+        });
+      });
+      service.bind('device', _data).subscribe({
+        next: () => onlyMessage('操作成功'),
+        error: () => onlyMessage('操作失败', 'error'),
+        complete: () => {
+          setLoading(false);
+          props.reload();
+          props.onCancel();
+        },
+      });
     } else {
       message.warn('请先勾选数据');
       // props.onCancel();
@@ -161,21 +186,58 @@ const Bind = observer((props: Props) => {
       visible={props.visible}
       onOk={handleBind}
       onCancel={props.onCancel}
+      confirmLoading={loading}
       width={'75vw'}
       title="绑定"
     >
-      <PermissionModal
-        type="device"
-        bindKeys={Models.bindKeys}
-        parentId={props.parentId}
-        ref={saveRef}
-        onCancel={(type) => {
-          if (type) {
-            props.reload();
-            props.onCancel();
-          }
-        }}
-      />
+      {/*<PermissionModal*/}
+      {/*  type="device"*/}
+      {/*  bindKeys={Models.bindKeys}*/}
+      {/*  parentId={props.parentId}*/}
+      {/*  ref={saveRef}*/}
+      {/*  onCancel={(type) => {*/}
+      {/*    if (type) {*/}
+      {/*      props.reload();*/}
+      {/*      props.onCancel();*/}
+      {/*    }*/}
+      {/*  }}*/}
+      {/*/>*/}
+      <div className={'assets-bind-tip'}>
+        <ExclamationCircleOutlined style={{ paddingRight: 8 }} />
+        只能分配有“共享”权限的资产数据
+      </div>
+      <div className={'assets-bind-switch'}>
+        <span>批量配置</span>
+        <Switch
+          checkedChildren="开"
+          unCheckedChildren="关"
+          onChange={(e) => {
+            setIsAll(e);
+            Store.set('assets-device', {
+              isAll: e,
+              assets: checkAssets,
+              bindKeys: Models.bindKeys,
+            });
+          }}
+        />
+      </div>
+      <div>
+        <Checkbox.Group
+          options={props.assetsType}
+          value={checkAssets}
+          onChange={(e: any) => {
+            Store.set('assets-device', {
+              isAll: isAll,
+              assets: e,
+              bindKeys: Models.bindKeys,
+            });
+            bindChecks.current.forEach((_, key) => {
+              bindChecks.current.set(key, e);
+            });
+            setCheckAssets(e);
+          }}
+        />
+      </div>
       <SearchComponent<DeviceItem>
         field={columns}
         enableSave={false}
@@ -225,7 +287,19 @@ const Bind = observer((props: Props) => {
           gridColumn={2}
           columnEmptyText={''}
           cardRender={(record) => (
-            <ExtraDeviceCard showBindBtn={false} showTool={false} {...record} cardType={'bind'} />
+            <ExtraDeviceCard
+              showBindBtn={false}
+              showTool={false}
+              {...record}
+              assetsOptions={props.assetsType}
+              allAssets={checkAssets}
+              cardType={'bind'}
+              onAssetsChange={(e) => {
+                if (bindChecks.current.has(record.id)) {
+                  bindChecks.current.set(record.id, e);
+                }
+              }}
+            />
           )}
           tableAlertRender={({ selectedRowKeys }) => <div>已选择 {selectedRowKeys.length} 项</div>}
           tableAlertOptionRender={() => {
@@ -249,9 +323,16 @@ const Bind = observer((props: Props) => {
                 Models.bindKeys = [
                   ...new Set([...Models.bindKeys, ...getSelectedRowsKey(selectedRows)]),
                 ];
+                bindChecks.current.set(record.id, checkAssets);
               } else {
                 Models.bindKeys = Models.bindKeys.filter((item) => item !== record.id);
+                bindChecks.current.delete(record.id);
               }
+              Store.set('assets-device', {
+                isAll: isAll,
+                assets: checkAssets,
+                bindKeys: Models.bindKeys,
+              });
             },
             onSelectAll: (selected, selectedRows, changeRows) => {
               if (selected) {
@@ -266,9 +347,43 @@ const Bind = observer((props: Props) => {
               }
             },
           }}
-          request={(params) =>
-            service.queryDeviceList({ ...params, sorts: [{ name: 'createTime', order: 'desc' }] })
-          }
+          request={async (params) => {
+            const resp: any = await service.queryDeviceList({
+              ...params,
+              sorts: [{ name: 'createTime', order: 'desc' }],
+            });
+            let newData = [];
+            if (resp.status === 200) {
+              newData = [...resp.result.data];
+              const assetsResp = await service.getBindingAssets(
+                'product',
+                resp.result.data.map((item: any) => item.id),
+              );
+              if (assetsResp.status === 200) {
+                newData = newData.map((item: any) => {
+                  const assetsItem = assetsResp.result.find(
+                    (aItem: any) => (aItem.assetId = item.id),
+                  );
+                  console.log(assetsItem);
+                  return {
+                    ...item,
+                    ...assetsItem,
+                  };
+                });
+              }
+            }
+
+            return {
+              code: resp.message,
+              result: {
+                data: newData as DeviceItem[],
+                pageIndex: 0,
+                pageSize: 0,
+                total: 0,
+              },
+              status: resp.status,
+            };
+          }}
           params={searchParam}
           height={'none'}
         />

+ 55 - 24
src/pages/system/Department/Assets/deivce/index.tsx

@@ -1,7 +1,7 @@
 // 资产分配-设备管理
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
 import { useIntl } from '@@/plugin-locale/localeExports';
-import { Badge } from 'antd';
+import { Badge, Button, Dropdown, Menu } from 'antd';
 import { useEffect, useRef, useState } from 'react';
 import { observer } from '@formily/react';
 import type { DeviceItem } from '@/pages/system/Department/typings';
@@ -42,9 +42,28 @@ export default observer((props: { parentId: string }) => {
   const [updateVisible, setUpdateVisible] = useState(false);
   const [updateId, setUpdateId] = useState('');
   const [permissions, setPermissions] = useState<string[]>([]);
+  const [assetsType, setAssetsType] = useState([]);
+
+  // 资产类型的权限定义
+  const getAssetsType = () => {
+    service.getAssetsType('device').then((res) => {
+      if (res.status === 200) {
+        setAssetsType(
+          res.result.map((item: any) => ({
+            label: item.name,
+            value: item.id,
+            disabled: item.id === 'read',
+          })),
+        );
+      } else {
+        setAssetsType([]);
+      }
+    });
+  };
 
   useEffect(() => {
     if (AssetsModel.tabsIndex === ASSETS_TABS_ENUM.Device && actionRef.current) {
+      getAssetsType();
       actionRef.current.reload();
     }
 
@@ -275,12 +294,44 @@ export default observer((props: { parentId: string }) => {
     Models.unBindKeys = [];
   }, [props.parentId]);
 
+  const menu = (
+    <Menu>
+      <Menu.Item key={'1'}>
+        <PermissionButton
+          icon={<DisconnectOutlined />}
+          key="unBind"
+          onClick={handleUnBind}
+          isPermission={permission.bind}
+        >
+          {intl.formatMessage({
+            id: 'pages.system.role.option.unBindUser',
+            defaultMessage: '批量解绑',
+          })}
+        </PermissionButton>
+      </Menu.Item>
+      <Menu.Item key={'2'}>
+        <PermissionButton
+          icon={<EditOutlined />}
+          key="update"
+          isPermission={permission.assert}
+          onClick={() => {
+            setUpdateId('');
+            setUpdateVisible(true);
+          }}
+        >
+          批量编辑
+        </PermissionButton>
+      </Menu.Item>
+    </Menu>
+  );
+
   return (
     <div className="content">
       {Models.bind && (
         <Bind
           visible={Models.bind}
           onCancel={closeModal}
+          assetsType={assetsType}
           reload={() => actionRef.current?.reload()}
           parentId={props.parentId}
         />
@@ -458,29 +509,9 @@ export default observer((props: { parentId: string }) => {
               defaultMessage: '资产分配',
             })}
           </PermissionButton>,
-          <PermissionButton
-            icon={<DisconnectOutlined />}
-            key="unBind"
-            popConfirm={{
-              title: intl.formatMessage({
-                id: 'pages.system.role.option.unBindUser',
-                defaultMessage: '是否批量解除绑定',
-              }),
-              onConfirm: handleUnBind,
-            }}
-            tooltip={{
-              title: intl.formatMessage({
-                id: 'pages.system.role.option.unBindUser',
-                defaultMessage: '批量解绑',
-              }),
-            }}
-            isPermission={permission.bind}
-          >
-            {intl.formatMessage({
-              id: 'pages.system.role.option.unBindUser',
-              defaultMessage: '批量解绑',
-            })}
-          </PermissionButton>,
+          <Dropdown overlay={menu} placement="bottom">
+            <Button>批量操作</Button>
+          </Dropdown>,
         ]}
       />
     </div>

+ 11 - 0
src/pages/system/Department/Assets/index.less

@@ -3,3 +3,14 @@
     padding-left: 0 !important;
   }
 }
+.assets-bind-tip {
+  padding: 12px;
+  background-color: #f6f6f6;
+}
+
+.assets-bind-switch {
+  margin: 16px 0;
+  span {
+    padding-right: 12px;
+  }
+}

+ 135 - 29
src/pages/system/Department/Assets/product/bind.tsx

@@ -1,23 +1,26 @@
 // 资产-产品分类-绑定
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
 import { service } from './index';
-import { Badge, message, Modal, Space } from 'antd';
+import { Badge, Checkbox, message, Modal, Space, Switch } from 'antd';
 import Models from './model';
 import { useEffect, useRef, useState } from 'react';
 import { observer } from '@formily/react';
 import { useIntl } from '@@/plugin-locale/localeExports';
-import PermissionModal from '@/pages/system/Department/Assets/permissionModal';
 import type { ProductItem } from '@/pages/system/Department/typings';
 import SearchComponent from '@/components/SearchComponent';
 import { ExtraProductCard } from '@/components/ProTableCard/CardItems/product';
 import { ProTableCard } from '@/components';
 import { AssetsModel } from '@/pages/system/Department/Assets';
+import { ExclamationCircleOutlined } from '@ant-design/icons';
+import { Store } from 'jetlinks-store';
+import { onlyMessage } from '@/utils/util';
 
 interface Props {
   reload: () => void;
   visible: boolean;
   onCancel: () => void;
   parentId: string;
+  assetsType: string[];
 }
 
 const status = {
@@ -29,8 +32,10 @@ const Bind = observer((props: Props) => {
   const intl = useIntl();
   const actionRef = useRef<ActionType>();
   const [searchParam, setSearchParam] = useState({});
-  const saveRef = useRef<{ saveData: Function }>();
   const [loading, setLoading] = useState(false);
+  const [checkAssets, setCheckAssets] = useState<string[]>(['read']);
+  const [isAll, setIsAll] = useState<boolean>(false);
+  const bindChecks = useRef(new Map());
 
   const columns: ProColumns<ProductItem>[] = [
     {
@@ -89,9 +94,27 @@ const Bind = observer((props: Props) => {
   ];
 
   const handleBind = () => {
-    if (Models.bindKeys.length && saveRef.current) {
+    if (Models.bindKeys.length) {
       setLoading(true);
-      saveRef.current?.saveData();
+      const _data: any[] = [];
+      bindChecks.current.forEach((value, key) => {
+        _data.push({
+          targetType: 'org',
+          targetId: props.parentId,
+          assetType: 'product',
+          assetIdList: [key],
+          permission: value,
+        });
+      });
+      service.bind('product', _data).subscribe({
+        next: () => onlyMessage('操作成功'),
+        error: () => onlyMessage('操作失败', 'error'),
+        complete: () => {
+          setLoading(false);
+          props.reload();
+          props.onCancel();
+        },
+      });
     } else {
       message.warn('请先勾选数据');
       // props.onCancel();
@@ -123,18 +146,54 @@ const Bind = observer((props: Props) => {
         title="绑定"
         confirmLoading={loading}
       >
-        <PermissionModal
-          type="product"
-          parentId={props.parentId}
-          bindKeys={Models.bindKeys}
-          ref={saveRef}
-          onCancel={(type) => {
-            if (type) {
-              props.reload();
-              props.onCancel();
-            }
-          }}
-        />
+        <div className={'assets-bind-tip'}>
+          <ExclamationCircleOutlined style={{ paddingRight: 8 }} />
+          只能分配有“共享”权限的资产数据
+        </div>
+        <div className={'assets-bind-switch'}>
+          <span>批量配置</span>
+          <Switch
+            checkedChildren="开"
+            unCheckedChildren="关"
+            onChange={(e) => {
+              setIsAll(e);
+              Store.set('assets-product', {
+                isAll: e,
+                assets: checkAssets,
+                bindKeys: Models.bindKeys,
+              });
+            }}
+          />
+        </div>
+        <div>
+          <Checkbox.Group
+            options={props.assetsType}
+            value={checkAssets}
+            onChange={(e: any) => {
+              Store.set('assets-product', {
+                isAll: isAll,
+                assets: e,
+                bindKeys: Models.bindKeys,
+              });
+              bindChecks.current.forEach((_, key) => {
+                bindChecks.current.set(key, e);
+              });
+              setCheckAssets(e);
+            }}
+          />
+        </div>
+        {/*<PermissionModal*/}
+        {/*  type="product"*/}
+        {/*  parentId={props.parentId}*/}
+        {/*  bindKeys={Models.bindKeys}*/}
+        {/*  ref={saveRef}*/}
+        {/*  onCancel={(type) => {*/}
+        {/*    if (type) {*/}
+        {/*      props.reload();*/}
+        {/*      props.onCancel();*/}
+        {/*    }*/}
+        {/*  }}*/}
+        {/*/>*/}
         <SearchComponent
           field={columns}
           model={'simple'}
@@ -206,9 +265,16 @@ const Bind = observer((props: Props) => {
                   Models.bindKeys = [
                     ...new Set([...Models.bindKeys, ...getSelectedRowsKey(selectedRows)]),
                   ];
+                  bindChecks.current.set(record.id, checkAssets);
                 } else {
                   Models.bindKeys = Models.bindKeys.filter((item) => item !== record.id);
+                  bindChecks.current.delete(record.id);
                 }
+                Store.set('assets-product', {
+                  isAll: isAll,
+                  assets: checkAssets,
+                  bindKeys: Models.bindKeys,
+                });
                 AssetsModel.params = {
                   productId: Models.bindKeys,
                 };
@@ -229,21 +295,61 @@ const Bind = observer((props: Props) => {
                 };
               },
             }}
-            request={(params) =>
-              service.queryProductList({
+            request={async (params) => {
+              const resp: any = await service.queryProductList({
                 ...params,
                 sorts: [{ name: 'createTime', order: 'desc' }],
-              })
-            }
+              });
+              let newData = [];
+              if (resp.status === 200) {
+                newData = [...resp.result.data];
+                const assetsResp = await service.getBindingAssets(
+                  'product',
+                  resp.result.data.map((item: any) => item.id),
+                );
+                if (assetsResp.status === 200) {
+                  newData = newData.map((item: any) => {
+                    const assetsItem = assetsResp.result.find(
+                      (aItem: any) => (aItem.assetId = item.id),
+                    );
+                    console.log(assetsItem);
+                    return {
+                      ...item,
+                      ...assetsItem,
+                    };
+                  });
+                }
+              }
+
+              return {
+                code: resp.message,
+                result: {
+                  data: newData as ProductItem[],
+                  pageIndex: 0,
+                  pageSize: 0,
+                  total: 0,
+                },
+                status: resp.status,
+              };
+            }}
             params={searchParam}
-            cardRender={(record) => (
-              <ExtraProductCard
-                showBindBtn={false}
-                showTool={false}
-                {...record}
-                cardType={'bind'}
-              />
-            )}
+            cardRender={(record) => {
+              return (
+                <ExtraProductCard
+                  showBindBtn={false}
+                  showTool={false}
+                  {...record}
+                  assetsOptions={props.assetsType}
+                  allAssets={checkAssets}
+                  cardType={'bind'}
+                  onAssetsChange={(e) => {
+                    if (bindChecks.current.has(record.id)) {
+                      bindChecks.current.set(record.id, e);
+                    }
+                  }}
+                />
+              );
+            }}
             height={'none'}
           />
         </div>

+ 70 - 24
src/pages/system/Department/Assets/product/index.tsx

@@ -1,7 +1,7 @@
 // 资产分配-产品分类
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
 import { useIntl } from '@@/plugin-locale/localeExports';
-import { Badge, Modal, Space } from 'antd';
+import { Badge, Button, Dropdown, Menu, Modal, Space } from 'antd';
 import { useEffect, useRef, useState } from 'react';
 import { observer } from '@formily/react';
 import type { ProductItem } from '@/pages/system/Department/typings';
@@ -36,9 +36,28 @@ export default observer((props: { parentId: string }) => {
   const [updateVisible, setUpdateVisible] = useState(false);
   const [updateId, setUpdateId] = useState('');
   const [permissions, setPermissions] = useState<string[]>([]);
+  const [assetsType, setAssetsType] = useState([]);
+
+  // 资产类型的权限定义
+  const getAssetsType = () => {
+    service.getAssetsType('product').then((res) => {
+      if (res.status === 200) {
+        setAssetsType(
+          res.result.map((item: any) => ({
+            label: item.name,
+            value: item.id,
+            disabled: item.id === 'read',
+          })),
+        );
+      } else {
+        setAssetsType([]);
+      }
+    });
+  };
 
   useEffect(() => {
     if (AssetsModel.tabsIndex === ASSETS_TABS_ENUM.Product && actionRef.current) {
+      getAssetsType();
       actionRef.current.reload();
     }
   }, [AssetsModel.tabsIndex]);
@@ -258,12 +277,59 @@ export default observer((props: { parentId: string }) => {
     });
   };
 
+  const menu = (
+    <Menu>
+      <Menu.Item key={'1'}>
+        <PermissionButton
+          icon={<DisconnectOutlined />}
+          key="unBind"
+          popConfirm={{
+            title: intl.formatMessage({
+              id: 'pages.system.role.option.unBindUser',
+              defaultMessage: '是否批量解除绑定',
+            }),
+            onConfirm: handleUnBind,
+          }}
+          isPermission={permission.bind}
+        >
+          {intl.formatMessage({
+            id: 'pages.system.role.option.unBindUser',
+            defaultMessage: '批量解绑',
+          })}
+        </PermissionButton>
+      </Menu.Item>
+      <Menu.Item key={'2'}>
+        <PermissionButton
+          icon={<EditOutlined />}
+          key="update"
+          isPermission={permission.assert}
+          popConfirm={{
+            title: intl.formatMessage({
+              id: 'pages.system.role.option.unBindUser',
+              defaultMessage: '是否批量解除绑定',
+            }),
+            onConfirm: () => {
+              if (Models.unBindKeys.length) {
+                setUpdateVisible(true);
+              } else {
+                onlyMessage('请勾选需要解绑的数据', 'warning');
+              }
+            },
+          }}
+        >
+          批量编辑
+        </PermissionButton>
+      </Menu.Item>
+    </Menu>
+  );
+
   return (
     <div className="content">
       {Models.bind && (
         <Bind
           visible={Models.bind}
           onCancel={closeModal}
+          assetsType={assetsType}
           reload={() => {
             setDeviceVisible(true);
             actionRef.current?.reload();
@@ -461,29 +527,9 @@ export default observer((props: { parentId: string }) => {
               defaultMessage: '资产分配',
             })}
           </PermissionButton>,
-          <PermissionButton
-            icon={<DisconnectOutlined />}
-            key="unBind"
-            popConfirm={{
-              title: intl.formatMessage({
-                id: 'pages.system.role.option.unBindUser',
-                defaultMessage: '是否批量解除绑定',
-              }),
-              onConfirm: handleUnBind,
-            }}
-            tooltip={{
-              title: intl.formatMessage({
-                id: 'pages.system.role.option.unBindUser',
-                defaultMessage: '批量解绑',
-              }),
-            }}
-            isPermission={permission.bind}
-          >
-            {intl.formatMessage({
-              id: 'pages.system.role.option.unBindUser',
-              defaultMessage: '批量解绑',
-            })}
-          </PermissionButton>,
+          <Dropdown overlay={menu} placement="bottom">
+            <Button>批量操作</Button>
+          </Dropdown>,
           // <Popconfirm
           //   title={intl.formatMessage({
           //     id: 'pages.system.role.option.unBindUser',

+ 6 - 0
src/pages/system/Department/Assets/service.ts

@@ -112,6 +112,12 @@ class Service<T> extends BaseService<T> {
       method: 'PUT',
       data: permission,
     });
+
+  getAssetsType = (type: string) =>
+    request(`${this.uri}/bindings/${type}/permissions`, { method: 'GET' });
+
+  getBindingAssets = (type: string, data: any[]) =>
+    request(`${this.uri}/bindings/${type}`, { method: 'POST', data });
 }
 
 export default Service;

+ 6 - 1
src/pages/system/Menu/Detail/buttons.tsx

@@ -369,7 +369,12 @@ export default (props: ButtonsProps) => {
                 defaultMessage: '说明',
               })}
             >
-              <Input.TextArea disabled={disabled} placeholder={'请输入说明'} />
+              <Input.TextArea
+                disabled={disabled}
+                placeholder={'请输入说明'}
+                maxLength={200}
+                showCount={true}
+              />
             </Form.Item>
           </Form>
         </Modal>

+ 4 - 2
src/pages/system/User/Save/index.tsx

@@ -331,9 +331,10 @@ const Save = (props: Props) => {
             'x-decorator': 'FormItem',
             'x-component': 'Select',
             'x-component-props': {
+              disabled: !rolePermission.view,
               mode: 'multiple',
               showArrow: true,
-              placeholder: '请选择角色',
+              placeholder: !rolePermission.view ? '暂无权限,请联系管理员' : '请选择角色',
               filterOption: (input: string, option: any) =>
                 option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0,
             },
@@ -373,9 +374,10 @@ const Save = (props: Props) => {
             'x-decorator': 'FormItem',
             'x-component': 'TreeSelect',
             'x-component-props': {
+              disabled: !deptPermission.view,
               multiple: true,
               showArrow: true,
-              placeholder: '请选择组织',
+              placeholder: !deptPermission.view ? '暂无权限,请联系管理员' : '请选择组织',
               showCheckedStrategy: ATreeSelect.SHOW_ALL,
               filterOption: (input: string, option: any) =>
                 option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0,