wzyyy 3 anni fa
parent
commit
4d97a86a39
52 ha cambiato i file con 2149 aggiunte e 931 eliminazioni
  1. 8 0
      src/components/Empty/index.less
  2. 11 0
      src/components/Empty/index.tsx
  3. 1 0
      src/components/Empty/style.ts
  4. 91 4
      src/components/ProTableCard/CardItems/device.tsx
  5. 94 2
      src/components/ProTableCard/CardItems/product.tsx
  6. 5 1
      src/components/ProTableCard/TableCard.tsx
  7. 38 1
      src/components/ProTableCard/index.less
  8. 85 24
      src/components/ProTableCard/index.tsx
  9. 1 0
      src/components/index.ts
  10. 13 11
      src/pages/account/NotificationSubscription/save/index.tsx
  11. 3 0
      src/pages/device/Category/index.tsx
  12. 5 1
      src/pages/device/Instance/Detail/ChildDevice/index.tsx
  13. 2 1
      src/pages/device/Instance/Detail/Diagnose/index.tsx
  14. 15 1
      src/pages/device/Instance/Detail/Functions/index.tsx
  15. 5 1
      src/pages/device/Instance/Detail/Info/index.tsx
  16. 5 2
      src/pages/device/Instance/Detail/Log/index.tsx
  17. 25 2
      src/pages/device/Instance/Detail/MetadataMap/EditableTable/index.tsx
  18. 78 55
      src/pages/device/Instance/Detail/MetadataMap/index.tsx
  19. 2 1
      src/pages/device/Instance/Detail/Modbus/index.tsx
  20. 2 1
      src/pages/device/Instance/Detail/Opcua/index.tsx
  21. 2 2
      src/pages/device/Instance/Detail/Running/Property/PropertyCard.tsx
  22. 1 1
      src/pages/device/Instance/Export/index.tsx
  23. 5 1
      src/pages/device/Product/Detail/index.tsx
  24. 5 1
      src/pages/device/components/Metadata/index.tsx
  25. 25 16
      src/pages/link/AccessConfig/index.tsx
  26. 9 1
      src/pages/link/Certificate/index.tsx
  27. 1 0
      src/pages/notice/Template/index.tsx
  28. 181 0
      src/pages/rule-engine/Alarm/Log/TabComponent/index copy.less
  29. 307 0
      src/pages/rule-engine/Alarm/Log/TabComponent/index copy.tsx
  30. 53 161
      src/pages/rule-engine/Alarm/Log/TabComponent/index.less
  31. 124 123
      src/pages/rule-engine/Alarm/Log/TabComponent/index.tsx
  32. 4 0
      src/pages/rule-engine/Alarm/Log/index.tsx
  33. 1 1
      src/pages/rule-engine/Alarm/Log/model.ts
  34. 11 9
      src/pages/system/Department/Assets/deivce/bind.tsx
  35. 87 15
      src/pages/system/Department/Assets/deivce/index.tsx
  36. 36 25
      src/pages/system/Department/Assets/index.tsx
  37. 16 23
      src/pages/system/Department/Assets/permissionModal.tsx
  38. 12 10
      src/pages/system/Department/Assets/product/bind.tsx
  39. 89 14
      src/pages/system/Department/Assets/product/index.tsx
  40. 7 8
      src/pages/system/Department/Assets/productCategory/bind.tsx
  41. 50 11
      src/pages/system/Department/Assets/productCategory/index.tsx
  42. 57 1
      src/pages/system/Department/Assets/service.ts
  43. 3 4
      src/pages/system/Department/Member/bind.tsx
  44. 39 12
      src/pages/system/Department/Member/index.tsx
  45. 3 0
      src/pages/system/Department/Tree/index.tsx
  46. 331 0
      src/pages/system/Department/Tree/tree.tsx
  47. 52 0
      src/pages/system/Department/index.less
  48. 122 370
      src/pages/system/Department/index.tsx
  49. 5 13
      src/pages/system/Department/save.tsx
  50. 1 0
      src/pages/system/Department/style.ts
  51. 3 1
      src/pages/system/Department/typings.d.ts
  52. 18 0
      src/utils/util.ts

+ 8 - 0
src/components/Empty/index.less

@@ -0,0 +1,8 @@
+.empty-body {
+  display: flex;
+  flex-direction: column;
+  align-content: center;
+  justify-content: center;
+  width: 100%;
+  height: 100%;
+}

+ 11 - 0
src/components/Empty/index.tsx

@@ -0,0 +1,11 @@
+import './style';
+import { Empty } from 'antd';
+import type { EmptyProps } from 'antd';
+
+export default (props: EmptyProps) => {
+  return (
+    <div className={'empty-body'}>
+      <Empty {...props} />
+    </div>
+  );
+};

+ 1 - 0
src/components/Empty/style.ts

@@ -0,0 +1 @@
+import './index.less';

+ 91 - 4
src/components/ProTableCard/CardItems/device.tsx

@@ -1,25 +1,112 @@
-import React from 'react';
+import React, { useState } from 'react';
 import type { DeviceInstance } from '@/pages/device/Instance/typings';
 import { StatusColorEnum } from '@/components/BadgeStatus';
 import { TableCard } from '@/components';
 import '@/style/common.less';
 import '../index.less';
+import { DisconnectOutlined } from '@ant-design/icons';
+import { Popconfirm } from 'antd';
+import { useIntl } from '@@/plugin-locale/localeExports';
 
-export interface DeviceCardProps extends DeviceInstance {
+export interface DeviceCardProps extends Partial<DeviceInstance> {
   detail?: React.ReactNode;
   actions?: React.ReactNode[];
   avatarSize?: number;
+  className?: string;
+  content?: React.ReactNode[];
+  onClick?: () => void;
+  grantedPermissions?: string[];
+  onUnBind?: (e: any) => void;
 }
 
 const defaultImage = require('/public/images/device-type-3-big.png');
 
+export const PermissionsMap = {
+  read: '查看',
+  save: '编辑',
+  delete: '删除',
+};
+
+export const handlePermissionsMap = (permissions?: string[]) => {
+  return permissions && permissions.length
+    ? permissions.map((item) => PermissionsMap[item]).toString()
+    : '--';
+};
+
+export const ExtraDeviceCard = (props: DeviceCardProps) => {
+  const intl = useIntl();
+  const [imgUrl, setImgUrl] = useState<string>(props.photoUrl || defaultImage);
+
+  return (
+    <TableCard
+      showTool={false}
+      showMask={false}
+      status={props.state?.value}
+      statusText={props.state?.text}
+      statusNames={{
+        online: StatusColorEnum.processing,
+        offline: StatusColorEnum.error,
+        notActive: StatusColorEnum.warning,
+      }}
+      onClick={props.onClick}
+      className={props.className}
+    >
+      <div className={'pro-table-card-item'}>
+        <div className={'card-item-avatar'}>
+          <img
+            width={88}
+            height={88}
+            src={imgUrl}
+            alt={''}
+            onError={() => {
+              setImgUrl(defaultImage);
+            }}
+          />
+        </div>
+        <div className={'card-item-body'}>
+          <div className={'card-item-header'}>
+            <span className={'card-item-header-name ellipsis'}>{props.name}</span>
+          </div>
+          <div className={'card-item-content-flex'}>
+            <div className={'flex-auto'}>
+              <label>ID</label>
+              <div className={'ellipsis'}>{props.id || '--'}</div>
+            </div>
+            <div className={'flex-auto'}>
+              <label>资产权限</label>
+              <div className={'ellipsis'}>{handlePermissionsMap(props.grantedPermissions)}</div>
+            </div>
+            <Popconfirm
+              title={intl.formatMessage({
+                id: 'pages.system.role.option.unBindUser',
+                defaultMessage: '是否解除绑定',
+              })}
+              key="unBind"
+              onConfirm={(e) => {
+                e?.stopPropagation();
+                if (props.onUnBind) {
+                  props.onUnBind(e);
+                }
+              }}
+            >
+              <div className={'flex-button'}>
+                <DisconnectOutlined />
+              </div>
+            </Popconfirm>
+          </div>
+        </div>
+      </div>
+    </TableCard>
+  );
+};
+
 export default (props: DeviceCardProps) => {
   return (
     <TableCard
       detail={props.detail}
       actions={props.actions}
-      status={props.state.value}
-      statusText={props.state.text}
+      status={props.state?.value}
+      statusText={props.state?.text}
       statusNames={{
         online: StatusColorEnum.processing,
         offline: StatusColorEnum.error,

+ 94 - 2
src/components/ProTableCard/CardItems/product.tsx

@@ -1,18 +1,110 @@
-import React from 'react';
+import React, { useState } from 'react';
 import type { ProductItem } from '@/pages/device/Product/typings';
 import { StatusColorEnum } from '@/components/BadgeStatus';
 import { useIntl } from 'umi';
 import { TableCard } from '@/components';
 import '@/style/common.less';
 import '../index.less';
+import { Popconfirm } from 'antd';
+import { DisconnectOutlined } from '@ant-design/icons';
 
-export interface ProductCardProps extends ProductItem {
+export interface ProductCardProps extends Partial<ProductItem> {
   detail?: React.ReactNode;
   actions?: React.ReactNode[];
   avatarSize?: number;
+  className?: string;
+  content?: React.ReactNode[];
+  onClick?: () => void;
+  grantedPermissions?: string[];
+  onUnBind?: (e: any) => void;
 }
+
 const defaultImage = require('/public/images/device-product.png');
 
+export const PermissionsMap = {
+  read: '查看',
+  save: '编辑',
+  delete: '删除',
+};
+
+export const handlePermissionsMap = (permissions?: string[]) => {
+  return permissions && permissions.length
+    ? permissions.map((item) => PermissionsMap[item]).toString()
+    : '--';
+};
+
+export const ExtraProductCard = (props: ProductCardProps) => {
+  const intl = useIntl();
+  const [imgUrl, setImgUrl] = useState<string>(props.photoUrl || defaultImage);
+
+  return (
+    <TableCard
+      showTool={false}
+      showMask={false}
+      status={props.state}
+      statusText={intl.formatMessage({
+        id: `pages.system.tenant.assetInformation.${props.state ? 'published' : 'unpublished'}`,
+        defaultMessage: '已发布',
+      })}
+      statusNames={{
+        0: StatusColorEnum.error,
+        1: StatusColorEnum.processing,
+      }}
+      className={props.className}
+      onClick={props.onClick}
+    >
+      <div className={'pro-table-card-item'}>
+        <div className={'card-item-avatar'}>
+          <img
+            width={88}
+            height={88}
+            src={imgUrl}
+            alt={''}
+            onError={() => {
+              setImgUrl(defaultImage);
+            }}
+          />
+        </div>
+        <div className={'card-item-body'}>
+          <div className={'card-item-header'}>
+            <span className={'card-item-header-name ellipsis'}>{props.name}</span>
+          </div>
+          <div className={'card-item-content-items'} style={{ display: 'flex', gap: 12 }}>
+            {props.content}
+          </div>
+          <div className={'card-item-content-flex'}>
+            <div className={'flex-auto'}>
+              <label>ID</label>
+              <div className={'ellipsis'}>{props.id || '--'}</div>
+            </div>
+            <div className={'flex-auto'}>
+              <label>资产权限</label>
+              <div className={'ellipsis'}>{handlePermissionsMap(props.grantedPermissions)}</div>
+            </div>
+            <Popconfirm
+              title={intl.formatMessage({
+                id: 'pages.system.role.option.unBindUser',
+                defaultMessage: '是否解除绑定',
+              })}
+              key="unBind"
+              onConfirm={(e) => {
+                e?.stopPropagation();
+                if (props.onUnBind) {
+                  props.onUnBind(e);
+                }
+              }}
+            >
+              <div className={'flex-button'}>
+                <DisconnectOutlined />
+              </div>
+            </Popconfirm>
+          </div>
+        </div>
+      </div>
+    </TableCard>
+  );
+};
+
 export default (props: ProductCardProps) => {
   const intl = useIntl();
   return (

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

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

+ 38 - 1
src/components/ProTableCard/index.less

@@ -6,6 +6,7 @@
 
 .pro-table-card {
   position: relative;
+  background-color: #fff;
 
   .pro-table-card-setting-item {
     color: rgba(0, 0, 0, 0.75);
@@ -51,6 +52,38 @@
           }
         }
 
+        .card-item-content-flex {
+          display: flex;
+          flex-wrap: wrap;
+          gap: 12px;
+
+          .flex-auto {
+            flex: 1 1 auto;
+          }
+
+          .flex-button {
+            display: flex;
+            flex: 0 0 50px;
+            align-items: center;
+            justify-content: center;
+            height: 50px;
+            color: @primary-color;
+            border: 1px solid @primary-color-active;
+            cursor: pointer;
+          }
+
+          label {
+            color: rgba(#000, 0.75);
+            font-size: 12px;
+          }
+
+          .ellipsis {
+            width: 70%;
+            font-weight: bold;
+            font-size: 14px;
+          }
+        }
+
         .card-item-content {
           display: flex;
           flex-wrap: wrap;
@@ -70,7 +103,7 @@
           }
 
           .ellipsis {
-            width: 100%;
+            width: 70%;
             font-weight: bold;
             font-size: 14px;
           }
@@ -108,6 +141,10 @@
   width: 100%;
   background-color: #fff;
 
+  &.item-active {
+    border: 1px solid @primary-color-active;
+  }
+
   &.hover {
     box-shadow: 0 0 24px rgba(#000, 0.1);
   }

+ 85 - 24
src/components/ProTableCard/index.tsx

@@ -1,13 +1,14 @@
 import type { ProTableProps } from '@jetlinks/pro-table';
 import ProTable from '@jetlinks/pro-table';
 import type { ParamsType } from '@ant-design/pro-provider';
-import React, { useEffect, useState } from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
 import { isFunction } from 'lodash';
 import { Empty, Pagination, Space } from 'antd';
 import { AppstoreOutlined, BarsOutlined } from '@ant-design/icons';
 import classNames from 'classnames';
 import LoadingComponent from '@ant-design/pro-layout/es/PageLoading';
 import './index.less';
+import { getDomFullHeight } from '@/utils/util';
 
 enum ModelEnum {
   TABLE = 'TABLE',
@@ -39,32 +40,83 @@ const ProTableCard = <
   const [column, setColumn] = useState(props.gridColumn || 4);
   const [loading, setLoading] = useState(false);
   const [dataLength, setDataLength] = useState<number>(0);
+  const [minHeight, setMinHeight] = useState(100);
 
   /**
    * 处理 Card
    * @param dataSource
    */
-  const handleCard = (dataSource: readonly T[] | undefined): JSX.Element => {
-    setDataLength(dataSource ? dataSource.length : 0);
-    return (
-      <>
-        {dataSource && dataSource.length ? (
-          <div
-            className={'pro-table-card-items'}
-            style={{ gridTemplateColumns: `repeat(${column}, 1fr)` }}
-          >
-            {dataSource.map((item) =>
-              cardRender && isFunction(cardRender) ? cardRender(item) : null,
-            )}
-          </div>
-        ) : (
-          <div style={{ display: 'flex', justifyContent: 'center' }}>
-            <Empty />
-          </div>
-        )}
-      </>
-    );
-  };
+  const handleCard = useCallback(
+    (dataSource: readonly T[] | undefined, rowSelection?: any): JSX.Element => {
+      setDataLength(dataSource ? dataSource.length : 0);
+
+      const Item = (dom: React.ReactNode) => {
+        if (!rowSelection || (rowSelection && !rowSelection.selectedRowKeys)) {
+          return dom;
+        }
+        const { selectedRowKeys, onChange } = rowSelection;
+
+        // @ts-ignore
+        const id = dom.props.id;
+
+        // @ts-ignore
+        return React.cloneElement(dom, {
+          // @ts-ignore
+          className: classNames(dom.props.className, {
+            'item-active': selectedRowKeys && selectedRowKeys.includes(id),
+          }),
+          key: id,
+          onClick: (e) => {
+            e.stopPropagation();
+            if (onChange) {
+              const isSelect = selectedRowKeys.includes(id);
+
+              if (isSelect) {
+                const nowRowKeys = selectedRowKeys.filter((key: string) => key !== id);
+                onChange(
+                  nowRowKeys,
+                  dataSource!.filter((item) => nowRowKeys.includes(item.id)),
+                );
+              } else {
+                const nowRowKeys = [...selectedRowKeys, id];
+                onChange(
+                  nowRowKeys,
+                  dataSource!.filter((item) => nowRowKeys.includes(item.id)),
+                );
+              }
+            }
+          },
+        });
+      };
+
+      return (
+        <>
+          {dataSource && dataSource.length ? (
+            <div
+              className={'pro-table-card-items'}
+              style={{ gridTemplateColumns: `repeat(${column}, 1fr)` }}
+            >
+              {dataSource.map((item) =>
+                cardRender && isFunction(cardRender) ? Item(cardRender(item)) : null,
+              )}
+            </div>
+          ) : (
+            <div
+              style={{
+                display: 'flex',
+                justifyContent: 'center',
+                alignItems: 'center',
+                minHeight: minHeight - 150,
+              }}
+            >
+              <Empty />
+            </div>
+          )}
+        </>
+      );
+    },
+    [minHeight],
+  );
 
   const windowChange = () => {
     if (window.innerWidth <= 1440) {
@@ -92,7 +144,15 @@ const ProTableCard = <
   }, [props.params]);
 
   return (
-    <div className={'pro-table-card'}>
+    <div
+      className={'pro-table-card'}
+      style={{ minHeight: minHeight }}
+      ref={(ref) => {
+        if (ref) {
+          setMinHeight(getDomFullHeight('pro-table-card'));
+        }
+      }}
+    >
       <ProTable<T, U, ValueType>
         {...extraProps}
         params={
@@ -103,6 +163,7 @@ const ProTableCard = <
             pageSize,
           } as any
         }
+        className={'pro-table-card-body'}
         options={model === ModelEnum.CARD ? false : props.options}
         request={async (param, sort, filter) => {
           if (request) {
@@ -169,7 +230,7 @@ const ProTableCard = <
         tableViewRender={
           model === ModelEnum.CARD
             ? (tableProps) => {
-                return handleCard(tableProps.dataSource);
+                return handleCard(tableProps.dataSource, extraProps?.rowSelection);
               }
             : undefined
         }

+ 1 - 0
src/components/index.ts

@@ -11,3 +11,4 @@ export { default as PermissionButton } from './PermissionButton';
 export { default as TitleComponent } from './TitleComponent';
 export { default as AMap } from './AMapComponent/amap';
 export { default as PathSimplifier } from './AMapComponent/PathSimplifier';
+export { default as Empty } from './Empty';

+ 13 - 11
src/pages/account/NotificationSubscription/save/index.tsx

@@ -18,27 +18,29 @@ const Save = (props: Props) => {
   const [data, setDada] = useState<any>(props.data || {});
   const [dataList, setDataList] = useState<any[]>([]);
 
+  const form = useMemo(
+    () =>
+      createForm({
+        validateFirst: true,
+        initialValues: data,
+      }),
+    [],
+  );
+
   useEffect(() => {
     if (props.data?.topicConfig) {
-      setDada({
+      const param = {
         ...props.data,
         topicConfig: {
           alarmConfigId: props.data?.topicConfig?.alarmConfigId.split(','),
           alarmConfigName: props.data?.topicConfig?.alarmConfigName.split(','),
         },
-      });
+      };
+      setDada({ ...param });
+      form.setValues(param);
     }
   }, [props.data]);
 
-  const form = useMemo(
-    () =>
-      createForm({
-        validateFirst: true,
-        initialValues: data,
-      }),
-    [],
-  );
-
   const queryProvidersList = () => service.getProvidersList();
 
   const queryAlarmConfigList = () => {

+ 3 - 0
src/pages/device/Category/index.tsx

@@ -12,6 +12,7 @@ import { observer } from '@formily/react';
 import type { Response } from '@/utils/typings';
 import SearchComponent from '@/components/SearchComponent';
 import { PermissionButton } from '@/components';
+import { getDomFullHeight } from '@/utils/util';
 
 export const service = new Service('device/category');
 
@@ -199,6 +200,8 @@ const Category = observer(() => {
             status: response.status,
           };
         }}
+        className={'device-category'}
+        tableStyle={{ minHeight: getDomFullHeight('device-category', 94) }}
         rowKey="id"
         columns={columns}
         onChange={(_, f, sorter: any) => {

+ 5 - 1
src/pages/device/Instance/Detail/ChildDevice/index.tsx

@@ -11,6 +11,7 @@ import BindChildDevice from './BindChildDevice';
 import moment from 'moment';
 import { Link } from 'umi';
 import { getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
+import { getDomFullHeight } from '@/utils/util';
 
 const ChildDevice = () => {
   const intl = useIntl();
@@ -121,7 +122,10 @@ const ChildDevice = () => {
   ];
 
   return (
-    <Card>
+    <Card
+      className={'device-detail-childDevice'}
+      style={{ minHeight: getDomFullHeight('device-detail-childDevice', 12) }}
+    >
       <SearchComponent<LogItem>
         field={[...columns]}
         target="child-device"

+ 2 - 1
src/pages/device/Instance/Detail/Diagnose/index.tsx

@@ -7,6 +7,7 @@ import './index.less';
 import classNames from 'classnames';
 import { Store } from 'jetlinks-store';
 import { DiagnoseStatusModel } from './Status/model';
+import { getDomFullHeight } from '@/utils/util';
 
 interface ListProps {
   key: string;
@@ -131,7 +132,7 @@ const Diagnose = () => {
     };
   }, []);
   return (
-    <Card className="diagnose">
+    <Card className="diagnose" style={{ minHeight: getDomFullHeight('diagnose', 12) }}>
       <div className={current === 'message' ? 'header-message' : 'header'}>
         <Row gutter={24} style={{ padding: 10, width: '100%' }}>
           {list.map((item: ListProps) => (

+ 15 - 1
src/pages/device/Instance/Detail/Functions/index.tsx

@@ -3,13 +3,22 @@ import { InstanceModel } from '@/pages/device/Instance';
 import type { FunctionMetadata } from '@/pages/device/Product/typings';
 import FnForm from './form';
 import AModel from './AdvancedMode';
+import { getDomFullHeight } from '@/utils/util';
+import { useEffect, useState } from 'react';
+import { Empty } from '@/components';
 
 const Functions = () => {
   const functionList = JSON.parse(InstanceModel.detail.metadata || '{}')
     .functions as FunctionMetadata[];
 
+  const [minHeight, setMinHeight] = useState(100);
+
+  useEffect(() => {
+    setMinHeight(getDomFullHeight('device-detail-function', 12));
+  }, []);
+
   return (
-    <Card>
+    <Card className={'device-detail-function'} style={{ minHeight: minHeight }}>
       <Tabs>
         <Tabs.TabPane tab={'精简模式'} key={1}>
           <Tabs tabPosition="left">
@@ -36,6 +45,11 @@ const Functions = () => {
           </Tabs>
         </Tabs.TabPane>
       </Tabs>
+      {!functionList && (
+        <div style={{ height: minHeight - 150 }}>
+          <Empty />
+        </div>
+      )}
     </Card>
   );
 };

+ 5 - 1
src/pages/device/Instance/Detail/Info/index.tsx

@@ -11,6 +11,7 @@ import type { DeviceInstance } from '../../typings';
 import { EditOutlined } from '@ant-design/icons';
 import Tags from '@/pages/device/Instance/Detail/Tags';
 import { PermissionButton } from '@/components';
+import { getDomFullHeight } from '@/utils/util';
 
 const Info = observer(() => {
   const intl = useIntl();
@@ -19,7 +20,10 @@ const Info = observer(() => {
 
   return (
     <>
-      <Card>
+      <Card
+        className={'device-detail-body'}
+        style={{ minHeight: getDomFullHeight('device-detail-body', 12) }}
+      >
         <Descriptions
           size="small"
           column={3}

+ 5 - 2
src/pages/device/Instance/Detail/Log/index.tsx

@@ -7,6 +7,7 @@ import { useIntl } from '@@/plugin-locale/localeExports';
 import { InstanceModel, service } from '@/pages/device/Instance';
 import { useRef, useState } from 'react';
 import SearchComponent from '@/components/SearchComponent';
+import { getDomFullHeight } from '@/utils/util';
 
 const Log = () => {
   const intl = useIntl();
@@ -38,7 +39,6 @@ const Log = () => {
     {
       title: '内容',
       dataIndex: 'content',
-      valueType: 'option',
       ellipsis: true,
       render: (text, record) => <span>{String(record.content)}</span>,
     },
@@ -78,7 +78,10 @@ const Log = () => {
   ];
 
   return (
-    <Card>
+    <Card
+      className={'device-detail-log'}
+      style={{ minHeight: getDomFullHeight('device-detail-log', 12) }}
+    >
       <SearchComponent<LogItem>
         field={[...columns]}
         target="logs"

+ 25 - 2
src/pages/device/Instance/Detail/MetadataMap/EditableTable/index.tsx

@@ -2,6 +2,8 @@ import React, { useContext, useEffect, useState } from 'react';
 import { Badge, Col, Form, Input, message, Pagination, Row, Select, Table, Tooltip } from 'antd';
 import { service } from '@/pages/device/Instance';
 import './index.less';
+import { QuestionCircleOutlined } from '@ant-design/icons';
+// import { throttle } from 'lodash';
 
 const defaultImage = require('/public/images/metadata-map.png');
 
@@ -49,7 +51,7 @@ const EditableCell = ({
       const values = await form.validateFields();
       handleSave({
         ...record,
-        originalId: values?.originalId,
+        originalId: !!values?.originalId ? values?.originalId : record.metadataId,
         customMapping: values?.originalId !== '',
       });
     } catch (errInfo) {
@@ -69,6 +71,20 @@ const EditableCell = ({
   if (editable) {
     childNode = (
       <Form.Item style={{ margin: 0 }} name={dataIndex}>
+        {/* <AutoComplete onChange={throttle(save, 5000)}>
+          <AutoComplete.Option value={''}>使用物模型属性</AutoComplete.Option>
+          {record.customMapping && temp && (
+            <AutoComplete.Option value={record.originalId}>
+              {temp?.name}({temp?.id})
+            </AutoComplete.Option>
+          )}
+          {tempList.length > 0 &&
+            tempList.map((item: any) => (
+              <AutoComplete.Option key={item?.id} value={item?.id}>
+                {item?.name}({item?.id})
+              </AutoComplete.Option>
+            ))}
+        </AutoComplete> */}
         <Select
           onChange={save}
           showSearch
@@ -118,7 +134,14 @@ const EditableTable = (props: Props) => {
       // render: (text: any, record: any) => <span>{`${record.name}(${record.id})`}</span>,
     },
     {
-      title: '设备上报属性',
+      title: (
+        <span>
+          设备上报属性
+          <Tooltip title="设备上报属性无法根据标识与物模型属性匹配时,默认使用物模型属性">
+            <QuestionCircleOutlined />
+          </Tooltip>
+        </span>
+      ),
       dataIndex: 'originalId',
       width: '30%',
       editable: true,

+ 78 - 55
src/pages/device/Instance/Detail/MetadataMap/index.tsx

@@ -1,11 +1,12 @@
-import { Card, Empty } from 'antd';
+import { Card } from 'antd';
 import { useEffect, useState } from 'react';
 import { service } from '@/pages/device/Instance';
 import EditableTable from './EditableTable';
 import { getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
 import type { ProductItem } from '@/pages/device/Product/typings';
 import { useParams } from 'umi';
-import { PermissionButton } from '@/components';
+import { PermissionButton, Empty } from '@/components';
+import { getDomFullHeight } from '@/utils/util';
 
 interface Props {
   type: 'device' | 'product';
@@ -17,6 +18,7 @@ const MetadataMap = (props: Props) => {
   const [data, setData] = useState<any>({});
   const params = useParams<{ id: string }>();
   const { permission } = PermissionButton.usePermission('device/Product');
+  const [minHeight, setMinHeight] = useState(300);
 
   const handleSearch = async () => {
     if (props.type === 'product') {
@@ -50,6 +52,7 @@ const MetadataMap = (props: Props) => {
   const renderComponent = () => {
     const metadata = JSON.parse(product?.metadata || '{}');
     const dmetadata = JSON.parse(data?.metadata || '{}');
+    const height = minHeight - 150;
     if (product) {
       const flag =
         (type === 'device' &&
@@ -59,24 +62,70 @@ const MetadataMap = (props: Props) => {
       if (!product.accessId && flag) {
         if (!permission.update) {
           return (
-            <Empty
-              description={<span>请联系管理员配置物模型属性,并选择对应产品的设备接入方式</span>}
-            />
+            <div style={{ height }}>
+              <Empty
+                description={<span>请联系管理员配置物模型属性,并选择对应产品的设备接入方式</span>}
+              />
+            </div>
           );
         } else {
           return (
+            <div style={{ height }}>
+              <Empty
+                description={
+                  <span>
+                    请先配置对应产品的
+                    <a
+                      onClick={() => {
+                        checkUrl('metadata');
+                      }}
+                    >
+                      物模型属性
+                    </a>
+                    ,并选择对应产品的
+                    <a
+                      onClick={() => {
+                        checkUrl('access');
+                      }}
+                    >
+                      设备接入方式
+                    </a>
+                  </span>
+                }
+              />
+            </div>
+          );
+        }
+      } else if (flag && product.accessId) {
+        return (
+          <div style={{ height }}>
+            <Empty
+              description={
+                !permission.update ? (
+                  <span>请联系管理员配置物模型属性</span>
+                ) : (
+                  <span>
+                    请配置对应产品的
+                    <a
+                      onClick={() => {
+                        checkUrl('metadata');
+                      }}
+                    >
+                      物模型属性
+                    </a>
+                  </span>
+                )
+              }
+            />
+          </div>
+        );
+      } else if (!flag && !product.accessId) {
+        return (
+          <div style={{ height }}>
             <Empty
               description={
                 <span>
-                  请先配置对应产品的
-                  <a
-                    onClick={() => {
-                      checkUrl('metadata');
-                    }}
-                  >
-                    物模型属性
-                  </a>
-                  ,并选择对应产品的
+                  请选择对应产品的
                   <a
                     onClick={() => {
                       checkUrl('access');
@@ -87,58 +136,32 @@ const MetadataMap = (props: Props) => {
                 </span>
               }
             />
-          );
-        }
-      } else if (flag && product.accessId) {
-        return (
-          <Empty
-            description={
-              !permission.update ? (
-                <span>请联系管理员配置物模型属性</span>
-              ) : (
-                <span>
-                  请配置对应产品的
-                  <a
-                    onClick={() => {
-                      checkUrl('metadata');
-                    }}
-                  >
-                    物模型属性
-                  </a>
-                </span>
-              )
-            }
-          />
-        );
-      } else if (!flag && !product.accessId) {
-        return (
-          <Empty
-            description={
-              <span>
-                请选择对应产品的
-                <a
-                  onClick={() => {
-                    checkUrl('access');
-                  }}
-                >
-                  设备接入方式
-                </a>
-              </span>
-            }
-          />
+          </div>
         );
       } else {
         return <EditableTable data={data} type={type} />;
       }
     }
-    return <Empty />;
+    return (
+      <div style={{ height }}>
+        <Empty />
+      </div>
+    );
   };
 
   useEffect(() => {
     handleSearch();
   }, [props.type]);
 
-  return <Card bordered={false}>{renderComponent()}</Card>;
+  useEffect(() => {
+    setMinHeight(getDomFullHeight('device-detail-metadataMap', 12));
+  }, []);
+
+  return (
+    <Card bordered={false} className="device-detail-metadataMap" style={{ minHeight }}>
+      {renderComponent()}
+    </Card>
+  );
 };
 
 export default MetadataMap;

+ 2 - 1
src/pages/device/Instance/Detail/Modbus/index.tsx

@@ -17,6 +17,7 @@ import { InstanceModel } from '@/pages/device/Instance';
 import AddPoint from '@/pages/link/Channel/Modbus/Access/addPoint';
 import useSendWebsocketMessage from '@/hooks/websocket/useSendWebsocketMessage';
 import { map } from 'rxjs/operators';
+import { getDomFullHeight } from '@/utils/util';
 
 const Modbus = () => {
   const intl = useIntl();
@@ -213,7 +214,7 @@ const Modbus = () => {
   }, [data]);
 
   return (
-    <Card className={styles.list}>
+    <Card className={styles.list} style={{ minHeight: getDomFullHeight(styles.list, 12) }}>
       <div style={{ display: 'flex' }}>
         <div>
           <div style={{ width: '250px', marginTop: 15 }}>

+ 2 - 1
src/pages/device/Instance/Detail/Opcua/index.tsx

@@ -17,6 +17,7 @@ import { InstanceModel } from '@/pages/device/Instance';
 import AddPoint from '@/pages/link/Channel/Opcua/Access/addPoint';
 import useSendWebsocketMessage from '@/hooks/websocket/useSendWebsocketMessage';
 import { map } from 'rxjs/operators';
+import { getDomFullHeight } from '@/utils/util';
 
 const Opcua = () => {
   const intl = useIntl();
@@ -231,7 +232,7 @@ const Opcua = () => {
   }, [data]);
 
   return (
-    <Card className={styles.list}>
+    <Card className={styles.list} style={{ minHeight: getDomFullHeight(styles.list, 12) }}>
       <div style={{ display: 'flex' }}>
         <div>
           <div style={{ width: '250px', marginTop: 15 }}>

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

@@ -49,7 +49,7 @@ const Property = (props: Props) => {
           <Tooltip title={title}>{title}</Tooltip>
         </div>
         <Space style={{ fontSize: 12 }}>
-          {data.expands?.type.includes('write') && (
+          {data.expands?.type?.includes('write') && (
             <Tooltip placement="top" title="设置属性至设备">
               <EditOutlined
                 onClick={() => {
@@ -70,7 +70,7 @@ const Property = (props: Props) => {
                 />
               </Tooltip>
             )}
-          {data.expands?.type.includes('read') && (
+          {data.expands?.type?.includes('read') && (
             <Tooltip placement="top" title="获取最新属性值">
               <SyncOutlined onClick={refreshProperty} />
             </Tooltip>

+ 1 - 1
src/pages/device/Instance/Export/index.tsx

@@ -116,7 +116,7 @@ const Export = (props: Props) => {
       onOk={downloadTemplate}
     >
       <Alert
-        message="不勾选产品,默认导出所有设备的基础数据,勾选单个产品可导出下属的详细数据"
+        message="选择单个产品时可导出其下属设备的详细数据,不选择产品时导出所有设备的基础数据"
         type="warning"
         showIcon
         closable

+ 5 - 1
src/pages/device/Product/Detail/index.tsx

@@ -17,6 +17,7 @@ import MetadataMap from '@/pages/device/Instance/Detail/MetadataMap';
 import SystemConst from '@/utils/const';
 import { PermissionButton } from '@/components';
 import { QuestionCircleOutlined } from '@ant-design/icons';
+import { getDomFullHeight } from '@/utils/util';
 
 export const ModelEnum = {
   base: 'base',
@@ -313,7 +314,10 @@ const ProductDetail = observer(() => {
         </PermissionButton>,
       ]}
     >
-      <Card>
+      <Card
+        className={'product-detail-body'}
+        style={{ minHeight: getDomFullHeight('product-detail-body', 12) }}
+      >
         {list.find((k) => k.key === mode)?.component}
         {/* <Tabs
           defaultActiveKey={ModelEnum.base}

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

@@ -14,6 +14,7 @@ import { PermissionButton } from '@/components';
 import { Store } from 'jetlinks-store';
 import SystemConst from '@/utils/const';
 import { useParams } from 'umi';
+import { getDomFullHeight } from '@/utils/util';
 
 interface Props {
   tabAction?: ReactNode;
@@ -44,7 +45,10 @@ const Metadata = observer((props: Props) => {
   };
 
   return (
-    <div style={{ position: 'relative' }}>
+    <div
+      className={'device-detail-metadata'}
+      style={{ position: 'relative', minHeight: getDomFullHeight('device-detail-metadata', 32) }}
+    >
       <div className={styles.tips}>
         <InfoCircleOutlined style={{ marginRight: '3px' }} />
         {InstanceModel.detail?.independentMetadata

+ 25 - 16
src/pages/link/AccessConfig/index.tsx

@@ -2,13 +2,14 @@ import SearchComponent from '@/components/SearchComponent';
 import { getMenuPathByCode, MENUS_CODE } from '@/utils/menu';
 import { PageContainer } from '@ant-design/pro-layout';
 import type { ProColumns } from '@jetlinks/pro-table';
-import { Card, Col, Empty, message, Pagination, Row } from 'antd';
+import { Card, Col, message, Pagination, Row } from 'antd';
 import { useEffect, useState } from 'react';
 import { useHistory } from 'umi';
 import Service from './service';
 import { DeleteOutlined, EditOutlined, PlayCircleOutlined, StopOutlined } from '@ant-design/icons';
 import AccessConfigCard from '@/components/ProTableCard/CardItems/AccessConfig';
-import { PermissionButton } from '@/components';
+import { PermissionButton, Empty } from '@/components';
+import { getDomFullHeight } from '@/utils/util';
 
 export const service = new Service('gateway/device');
 
@@ -16,6 +17,7 @@ const AccessConfig = () => {
   const history = useHistory();
   const [param, setParam] = useState<any>({ pageSize: 10, terms: [] });
   const { permission } = PermissionButton.usePermission('link/AccessConfig');
+  const [minHeight, setMinHeight] = useState(100);
 
   const columns: ProColumns<any>[] = [
     {
@@ -67,20 +69,25 @@ const AccessConfig = () => {
 
   return (
     <PageContainer>
-      <Card>
-        <SearchComponent
-          field={columns}
-          // enableSave={false}
-          target={'access-config'}
-          onSearch={(data: any) => {
-            const dt = {
-              pageSize: 10,
-              terms: [...data?.terms],
-            };
-            handleSearch(dt);
+      <SearchComponent
+        field={columns}
+        // enableSave={false}
+        target={'access-config'}
+        onSearch={(data: any) => {
+          const dt = {
+            pageSize: 10,
+            terms: [...data?.terms],
+          };
+          handleSearch(dt);
+        }}
+      />
+      <Card className={'link-accessConfig'} style={{ minHeight }}>
+        <div
+          style={{ width: '100%', display: 'flex', justifyContent: 'flex-start' }}
+          ref={() => {
+            setMinHeight(getDomFullHeight('link-accessConfig'));
           }}
-        />
-        <div style={{ width: '100%', display: 'flex', justifyContent: 'flex-start' }}>
+        >
           <PermissionButton
             isPermission={permission.add}
             onClick={() => {
@@ -177,7 +184,9 @@ const AccessConfig = () => {
             ))}
           </Row>
         ) : (
-          <Empty />
+          <div style={{ height: minHeight - 150 }}>
+            <Empty />
+          </div>
         )}
         {dataSource.data.length > 0 && (
           <div style={{ display: 'flex', marginTop: 20, justifyContent: 'flex-end' }}>

+ 9 - 1
src/pages/link/Certificate/index.tsx

@@ -1,5 +1,5 @@
 import { PageContainer } from '@ant-design/pro-layout';
-import { useRef, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
 import ProTable from '@jetlinks/pro-table';
 import { message, Tooltip } from 'antd';
@@ -11,6 +11,7 @@ import usePermissions from '@/hooks/permission';
 import { getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
 import { history } from 'umi';
 import Service from '../service';
+import { getDomFullHeight } from '@/utils/util';
 
 export const service = new Service('network/certificate');
 
@@ -19,6 +20,7 @@ const Certificate = () => {
   const actionRef = useRef<ActionType>();
   const [param, setParam] = useState({});
   const { permission } = usePermissions('link/Certificate');
+  const [minHeight, setMinHeight] = useState(100);
 
   const columns: ProColumns<CertificateItem>[] = [
     {
@@ -106,6 +108,10 @@ const Certificate = () => {
     },
   ];
 
+  useEffect(() => {
+    setMinHeight(getDomFullHeight('link-certificate', 94));
+  }, []);
+
   return (
     <PageContainer>
       <SearchComponent<CertificateItem>
@@ -124,6 +130,8 @@ const Certificate = () => {
         search={false}
         scroll={{ x: 1366 }}
         rowKey="id"
+        className={'link-certificate'}
+        tableStyle={{ minHeight }}
         headerTitle={
           <PermissionButton
             onClick={() => {

+ 1 - 0
src/pages/notice/Template/index.tsx

@@ -251,6 +251,7 @@ const Template = observer(() => {
             <Upload
               disabled={!templatePermission.import}
               key={'import'}
+              accept=".json"
               showUploadList={false}
               beforeUpload={(file) => {
                 const reader = new FileReader();

+ 181 - 0
src/pages/rule-engine/Alarm/Log/TabComponent/index copy.less

@@ -0,0 +1,181 @@
+@import '~antd/es/style/themes/default.less';
+
+@alarm-log-padding-left: 60px;
+
+.ellipsis {
+  width: 100%;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+}
+
+.alarm-log-card {
+  .alarm-log-item {
+    display: flex;
+    margin-bottom: 20px;
+
+    .alarm-log-title {
+      position: relative;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      width: 15%;
+      padding: 10px;
+      overflow: hidden;
+      color: @primary-color;
+      background-color: #f0f2f3;
+      .alarm-log-level {
+        position: absolute;
+        top: 10px;
+        right: -12px;
+        display: flex;
+        justify-content: center;
+        width: 100px;
+        padding: 2px 0;
+        color: white;
+        background-color: red;
+        transform: skewX(45deg);
+        .alarm-log-text {
+          transform: skewX(-45deg);
+        }
+      }
+      .alarm-log-title-text {
+        margin: 0 10px;
+        .ellipsis();
+      }
+    }
+    .alarm-log-content {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      width: 87%;
+      padding: 20px;
+      background: url('/images/alarm/background.png') no-repeat;
+      background-size: 100% 100%;
+
+      .alarm-log-data {
+        display: flex;
+        align-items: center;
+        min-width: 60%;
+        max-width: 100%;
+
+        .alarm-log-image {
+          display: flex;
+          align-items: center;
+          padding-right: @alarm-log-padding-left;
+          border-right: 1px solid rgba(0, 0, 0, 0.09);
+
+          .alarm-type {
+            width: 40px;
+
+            .name {
+              color: #000;
+              font-size: 18px;
+            }
+
+            .text {
+              margin-top: 8px;
+              color: #666;
+              font-size: 14px;
+              .ellipsis();
+            }
+          }
+        }
+        .alarm-log-right {
+          display: flex;
+          justify-content: space-between;
+          width: 50%;
+          padding-left: @alarm-log-padding-left;
+
+          .alarm-log-time {
+            max-width: 165px;
+
+            .log-title {
+              margin-top: 8px;
+              color: #666;
+              font-size: 12px;
+            }
+
+            .context {
+              margin-top: 8px;
+              color: rgba(#000, 0.85);
+              font-size: 14px;
+              .ellipsis();
+            }
+          }
+
+          .alarm-log-status {
+            margin-left: @alarm-log-padding-left;
+          }
+        }
+      }
+      .alarm-log-actions {
+        .alarm-log-action {
+          display: flex;
+          justify-content: center;
+          width: 72px;
+          height: 72px;
+          background-color: #fff;
+          border: 1px solid @primary-color;
+
+          .btn {
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            justify-content: center;
+            width: 72px;
+            height: 72px;
+            .icon {
+              margin-bottom: 5px;
+              color: @primary-color;
+              font-size: 25px;
+            }
+
+            div {
+              color: @primary-color;
+              font-size: 12px;
+            }
+          }
+        }
+
+        .alarm-log-action:hover {
+          background-color: @primary-color;
+
+          .icon,
+          div {
+            color: #fff;
+          }
+        }
+        .alarm-log-disabled {
+          display: flex;
+          justify-content: center;
+          width: 72px;
+          height: 72px;
+          border: 1px solid rgba(0, 0, 0, 0.25);
+          .btn {
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            justify-content: center;
+            width: 72px;
+            height: 72px;
+            .icon {
+              margin-bottom: 5px;
+              color: rgba(0, 0, 0, 0.25);
+              font-size: 25px;
+            }
+
+            div {
+              color: rgba(0, 0, 0, 0.25);
+              font-size: 12px;
+            }
+          }
+        }
+      }
+    }
+  }
+
+  .alarm-log-item:hover {
+    box-shadow: 0 2px 16px rgba(0, 0, 0, 0.1);
+  }
+}

+ 307 - 0
src/pages/rule-engine/Alarm/Log/TabComponent/index copy.tsx

@@ -0,0 +1,307 @@
+import SearchComponent from '@/components/SearchComponent';
+import { FileFilled, FileTextFilled, ToolFilled } from '@ant-design/icons';
+import type { ProColumns } from '@jetlinks/pro-table';
+import { Badge, Button, Card, Col, Empty, Pagination, Row, Space, Tooltip } from 'antd';
+import { useEffect, useState } from 'react';
+import './index.less';
+import SolveComponent from '../SolveComponent';
+import SolveLog from '../SolveLog';
+import { AlarmLogModel } from '../model';
+import moment from 'moment';
+import { observer } from '@formily/reactive-react';
+import { service } from '@/pages/rule-engine/Alarm/Log';
+import { getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
+import { useHistory } from 'umi';
+import PermissionButton from '@/components/PermissionButton';
+import classNames from 'classnames';
+
+interface Props {
+  type: string;
+}
+
+const imgMap = new Map();
+imgMap.set('product', require('/public/images/alarm/product.png'));
+imgMap.set('device', require('/public/images/alarm/device.png'));
+imgMap.set('other', require('/public/images/alarm/other.png'));
+imgMap.set('org', require('/public/images/alarm/org.png'));
+
+const titleMap = new Map();
+titleMap.set('product', '产品');
+titleMap.set('device', '设备');
+titleMap.set('other', '其他');
+titleMap.set('org', '部门');
+
+const colorMap = new Map();
+colorMap.set(1, '#E50012');
+colorMap.set(2, '#FF9457');
+colorMap.set(3, '#FABD47');
+colorMap.set(4, '#999999');
+colorMap.set(5, '#C4C4C4');
+
+const TabComponent = observer((props: Props) => {
+  const { permission } = PermissionButton.usePermission('rule-engine/Alarm/Log');
+  const columns: ProColumns<any>[] = [
+    {
+      title: '名称',
+      dataIndex: 'alarmName',
+    },
+    {
+      title: '最近告警时间',
+      dataIndex: 'alarmTime',
+      valueType: 'dateTime',
+    },
+    {
+      title: '状态',
+      dataIndex: 'state',
+      valueType: 'select',
+      valueEnum: {
+        warning: {
+          text: '告警中',
+          status: 'warning',
+        },
+        normal: {
+          text: '无告警',
+          status: 'normal',
+        },
+      },
+    },
+  ];
+
+  const [param, setParam] = useState<any>({ pageSize: 10, terms: [] });
+  const history = useHistory<Record<string, string>>();
+
+  const [dataSource, setDataSource] = useState<any>({
+    data: [],
+    pageSize: 10,
+    pageIndex: 0,
+    total: 0,
+  });
+
+  const handleSearch = (params: any) => {
+    setParam(params);
+    const terms = [...params.terms];
+    if (props.type !== 'all') {
+      terms.push({
+        termType: 'eq',
+        column: 'targetType',
+        value: props.type,
+        type: 'and',
+      });
+    }
+    service
+      .query({
+        ...params,
+        terms: [...terms],
+        sorts: [{ name: 'alarmTime', order: 'desc' }],
+      })
+      .then((resp) => {
+        if (resp.status === 200) {
+          setDataSource(resp.result);
+        }
+      });
+  };
+
+  useEffect(() => {
+    handleSearch(param);
+  }, [props.type]);
+
+  return (
+    <div className="alarm-log-card">
+      <SearchComponent<any>
+        field={columns}
+        target="alarm-log"
+        onSearch={(data) => {
+          const dt = {
+            pageSize: 10,
+            terms: [...data?.terms],
+          };
+          handleSearch(dt);
+        }}
+      />
+      <Card>
+        {dataSource?.data.length > 0 ? (
+          <Row gutter={24} style={{ marginTop: 10 }}>
+            {(dataSource?.data || []).map((item: any) => (
+              <Col key={item.id} span={24}>
+                <div className="alarm-log-item">
+                  <div className="alarm-log-title">
+                    <div
+                      className="alarm-log-level"
+                      style={{ backgroundColor: colorMap.get(item.level) }}
+                    >
+                      <div className="alarm-log-text">
+                        {AlarmLogModel.defaultLevel.find((i) => i.level === item.level)?.title ||
+                          item.level}
+                      </div>
+                    </div>
+                    <div className="alarm-log-title-text">
+                      <Tooltip placement="topLeft" title={item.alarmName}>
+                        {item.alarmName}
+                      </Tooltip>
+                    </div>
+                  </div>
+                  <div className="alarm-log-content">
+                    <div className="alarm-log-data">
+                      <div className="alarm-log-image">
+                        <img
+                          width={88}
+                          height={88}
+                          src={imgMap.get(props.type)}
+                          alt={''}
+                          style={{ marginRight: 20 }}
+                        />
+                        <div className="alarm-type">
+                          <div className="name">{titleMap.get(item.targetType)}</div>
+                          <div className="text">
+                            <Tooltip placement="topLeft" title={item.targetName}>
+                              {item.targetName}
+                            </Tooltip>
+                          </div>
+                        </div>
+                      </div>
+                      <div className="alarm-log-right">
+                        <div className="alarm-log-time">
+                          <div className="log-title">最近告警时间</div>
+                          <div className="context">
+                            {moment(item.alarmTime).format('YYYY-MM-DD HH:mm:ss')}
+                          </div>
+                        </div>
+                        <div
+                          className="alarm-log-time alarm-log-status"
+                          style={{ paddingLeft: 10 }}
+                        >
+                          <div className="log-title">状态</div>
+                          <div className="context">
+                            <Badge status={item.state.value === 'warning' ? 'error' : 'default'} />
+                            <span
+                              style={{
+                                color: item.state.value === 'warning' ? '#E50012' : 'black',
+                              }}
+                            >
+                              {item.state.text}
+                            </span>
+                          </div>
+                        </div>
+                      </div>
+                    </div>
+                    <div className="alarm-log-actions">
+                      <Space>
+                        {item.state.value === 'warning' && (
+                          <div
+                            className={classNames(
+                              permission.action ? 'alarm-log-action' : 'alarm-log-disabled',
+                            )}
+                          >
+                            <Tooltip title={permission.action ? '' : '暂无权限,请联系管理员'}>
+                              <Button
+                                type={'link'}
+                                disabled={!permission.action}
+                                onClick={() => {
+                                  AlarmLogModel.solveVisible = true;
+                                  AlarmLogModel.current = item;
+                                }}
+                              >
+                                <div className="btn">
+                                  <ToolFilled className="icon" />
+                                  <div>告警处理</div>
+                                </div>
+                              </Button>
+                            </Tooltip>
+                          </div>
+                        )}
+                        <div className="alarm-log-action">
+                          <Button
+                            type={'link'}
+                            onClick={() => {
+                              AlarmLogModel.current = item;
+                              const url = getMenuPathByParams(
+                                MENUS_CODE['rule-engine/Alarm/Log/Detail'],
+                                item.id,
+                              );
+                              history.push(url);
+                            }}
+                          >
+                            <div className="btn">
+                              <FileFilled className="icon" />
+                              <div>告警日志</div>
+                            </div>
+                          </Button>
+                        </div>
+                        <div className="alarm-log-action">
+                          <Button
+                            type={'link'}
+                            onClick={() => {
+                              AlarmLogModel.logVisible = true;
+                              AlarmLogModel.current = item;
+                            }}
+                          >
+                            <div className="btn">
+                              <FileTextFilled className="icon" />
+                              <div>处理记录</div>
+                            </div>
+                          </Button>
+                        </div>
+                      </Space>
+                    </div>
+                  </div>
+                </div>
+              </Col>
+            ))}
+          </Row>
+        ) : (
+          <Empty />
+        )}
+        {dataSource.data.length > 0 && (
+          <div style={{ display: 'flex', marginTop: 20, justifyContent: 'flex-end' }}>
+            <Pagination
+              showSizeChanger
+              size="small"
+              className={'pro-table-card-pagination'}
+              total={dataSource?.total || 0}
+              current={dataSource?.pageIndex + 1}
+              onChange={(page, size) => {
+                handleSearch({
+                  ...param,
+                  pageIndex: page - 1,
+                  pageSize: size,
+                });
+              }}
+              pageSizeOptions={[10, 20, 50, 100]}
+              pageSize={dataSource?.pageSize}
+              showTotal={(num) => {
+                const minSize = dataSource?.pageIndex * dataSource?.pageSize + 1;
+                const MaxSize = (dataSource?.pageIndex + 1) * dataSource?.pageSize;
+                return `第 ${minSize} - ${MaxSize > num ? num : MaxSize} 条/总共 ${num} 条`;
+              }}
+            />
+          </div>
+        )}
+      </Card>
+      {AlarmLogModel.solveVisible && (
+        <SolveComponent
+          close={() => {
+            AlarmLogModel.solveVisible = false;
+            AlarmLogModel.current = {};
+          }}
+          reload={() => {
+            AlarmLogModel.solveVisible = false;
+            AlarmLogModel.current = {};
+            handleSearch(param);
+          }}
+          data={AlarmLogModel.current}
+        />
+      )}
+      {AlarmLogModel.logVisible && (
+        <SolveLog
+          close={() => {
+            AlarmLogModel.logVisible = false;
+            AlarmLogModel.current = {};
+          }}
+          data={AlarmLogModel.current}
+        />
+      )}
+    </div>
+  );
+});
+
+export default TabComponent;

+ 53 - 161
src/pages/rule-engine/Alarm/Log/TabComponent/index.less

@@ -1,181 +1,73 @@
 @import '~antd/es/style/themes/default.less';
 
-@alarm-log-padding-left: 60px;
+@card-content-padding-top: 30px;
 
-.ellipsis {
+.alarm-log-context {
+  display: flex;
+  align-items: center;
   width: 100%;
-  overflow: hidden;
-  white-space: nowrap;
-  text-overflow: ellipsis;
-}
 
-.alarm-log-card {
-  .alarm-log-item {
+  .context-left {
     display: flex;
-    margin-bottom: 20px;
+    align-items: center;
+    justify-content: space-between;
+    width: calc(40% - 40px);
+    margin-right: 10%;
 
-    .alarm-log-title {
-      position: relative;
-      display: flex;
-      align-items: center;
-      justify-content: center;
-      width: 15%;
-      padding: 10px;
-      overflow: hidden;
-      color: @primary-color;
-      background-color: #f0f2f3;
-      .alarm-log-level {
-        position: absolute;
-        top: 10px;
-        right: -12px;
-        display: flex;
-        justify-content: center;
-        width: 100px;
-        padding: 2px 0;
-        color: white;
-        background-color: red;
-        transform: skewX(45deg);
-        .alarm-log-text {
-          transform: skewX(-45deg);
-        }
-      }
-      .alarm-log-title-text {
-        margin: 0 10px;
-        .ellipsis();
-      }
+    .context-img {
+      margin-right: 20px;
     }
-    .alarm-log-content {
-      display: flex;
-      align-items: center;
-      justify-content: space-between;
-      width: 87%;
-      padding: 20px;
-      background: url('/images/alarm/background.png') no-repeat;
-      background-size: 100% 100%;
-
-      .alarm-log-data {
-        display: flex;
-        align-items: center;
-        min-width: 60%;
-        max-width: 100%;
-
-        .alarm-log-image {
-          display: flex;
-          align-items: center;
-          padding-right: @alarm-log-padding-left;
-          border-right: 1px solid rgba(0, 0, 0, 0.09);
-
-          .alarm-type {
-            width: 40px;
-
-            .name {
-              color: #000;
-              font-size: 18px;
-            }
-
-            .text {
-              margin-top: 8px;
-              color: #666;
-              font-size: 14px;
-              .ellipsis();
-            }
-          }
-        }
-        .alarm-log-right {
-          display: flex;
-          justify-content: space-between;
-          width: 50%;
-          padding-left: @alarm-log-padding-left;
-
-          .alarm-log-time {
-            max-width: 165px;
 
-            .log-title {
-              margin-top: 8px;
-              color: #666;
-              font-size: 12px;
-            }
+    .left-item {
+      width: calc(100% - 90px);
 
-            .context {
-              margin-top: 8px;
-              color: rgba(#000, 0.85);
-              font-size: 14px;
-              .ellipsis();
-            }
-          }
-
-          .alarm-log-status {
-            margin-left: @alarm-log-padding-left;
-          }
-        }
+      .left-item-title {
+        font-size: 18px;
       }
-      .alarm-log-actions {
-        .alarm-log-action {
-          display: flex;
-          justify-content: center;
-          width: 72px;
-          height: 72px;
-          background-color: #fff;
-          border: 1px solid @primary-color;
-
-          .btn {
-            display: flex;
-            flex-direction: column;
-            align-items: center;
-            justify-content: center;
-            width: 72px;
-            height: 72px;
-            .icon {
-              margin-bottom: 5px;
-              color: @primary-color;
-              font-size: 25px;
-            }
-
-            div {
-              color: @primary-color;
-              font-size: 12px;
-            }
-          }
-        }
 
-        .alarm-log-action:hover {
-          background-color: @primary-color;
+      .left-item-value {
+        color: #666;
+        font-size: 14px;
+      }
+    }
+  }
 
-          .icon,
-          div {
-            color: #fff;
-          }
-        }
-        .alarm-log-disabled {
-          display: flex;
-          justify-content: center;
-          width: 72px;
-          height: 72px;
-          border: 1px solid rgba(0, 0, 0, 0.25);
-          .btn {
-            display: flex;
-            flex-direction: column;
-            align-items: center;
-            justify-content: center;
-            width: 72px;
-            height: 72px;
-            .icon {
-              margin-bottom: 5px;
-              color: rgba(0, 0, 0, 0.25);
-              font-size: 25px;
-            }
+  .context-right {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    width: 60%;
+    margin-right: 10px;
+    padding-left: 10%;
+    border-left: 1px solid rgba(0, 0, 0, 0.09);
+
+    .right-item {
+      .right-item-title {
+        color: #666;
+        font-size: 12px;
+      }
 
-            div {
-              color: rgba(0, 0, 0, 0.25);
-              font-size: 12px;
-            }
-          }
-        }
+      .right-item-value {
+        color: #000;
+        font-size: 14px;
       }
     }
   }
+}
 
-  .alarm-log-item:hover {
-    box-shadow: 0 2px 16px rgba(0, 0, 0, 0.1);
+.alarm-log-state {
+  position: absolute;
+  top: @card-content-padding-top;
+  right: -12px;
+  display: flex;
+  justify-content: center;
+  width: 100px;
+  padding: 2px 0;
+  color: #fff;
+  background-color: rgba(#5995f5, 0.15);
+  transform: skewX(45deg);
+
+  .card-state-content {
+    transform: skewX(-45deg);
   }
 }

+ 124 - 123
src/pages/rule-engine/Alarm/Log/TabComponent/index.tsx

@@ -1,7 +1,7 @@
 import SearchComponent from '@/components/SearchComponent';
 import { FileFilled, FileTextFilled, ToolFilled } from '@ant-design/icons';
 import type { ProColumns } from '@jetlinks/pro-table';
-import { Badge, Button, Card, Col, Empty, Pagination, Row, Space, Tooltip } from 'antd';
+import { Badge, Button, Card, Col, Empty, Pagination, Row, Tooltip } from 'antd';
 import { useEffect, useState } from 'react';
 import './index.less';
 import SolveComponent from '../SolveComponent';
@@ -12,7 +12,6 @@ import { observer } from '@formily/reactive-react';
 import { service } from '@/pages/rule-engine/Alarm/Log';
 import { getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
 import { useHistory } from 'umi';
-import PermissionButton from '@/components/PermissionButton';
 import classNames from 'classnames';
 
 interface Props {
@@ -39,7 +38,6 @@ colorMap.set(4, '#999999');
 colorMap.set(5, '#C4C4C4');
 
 const TabComponent = observer((props: Props) => {
-  const { permission } = PermissionButton.usePermission('rule-engine/Alarm/Log');
   const columns: ProColumns<any>[] = [
     {
       title: '名称',
@@ -79,18 +77,19 @@ const TabComponent = observer((props: Props) => {
 
   const handleSearch = (params: any) => {
     setParam(params);
+    const terms = [...params.terms];
+    if (props.type !== 'all') {
+      terms.push({
+        termType: 'eq',
+        column: 'targetType',
+        value: props.type,
+        type: 'and',
+      });
+    }
     service
       .query({
         ...params,
-        terms: [
-          ...params.terms,
-          {
-            termType: 'eq',
-            column: 'targetType',
-            value: props.type,
-            type: 'and',
-          },
-        ],
+        terms: [...terms],
         sorts: [{ name: 'alarmTime', order: 'desc' }],
       })
       .then((resp) => {
@@ -104,6 +103,68 @@ const TabComponent = observer((props: Props) => {
     handleSearch(param);
   }, [props.type]);
 
+  const tools = (record: any) => [
+    <Button
+      type={'link'}
+      key={'solve'}
+      style={{ padding: 0 }}
+      disabled={record.state.value === 'normal'}
+      onClick={() => {
+        AlarmLogModel.solveVisible = true;
+        AlarmLogModel.current = record;
+      }}
+    >
+      <Tooltip title={record.state?.value === 'normal' ? '无告警' : ''}>
+        <ToolFilled />
+        告警处理
+      </Tooltip>
+    </Button>,
+    <Button
+      type={'link'}
+      style={{ padding: 0 }}
+      key={'log'}
+      onClick={() => {
+        AlarmLogModel.current = record;
+        const url = getMenuPathByParams(MENUS_CODE['rule-engine/Alarm/Log/Detail'], record.id);
+        history.push(url);
+      }}
+    >
+      <FileFilled />
+      告警日志
+    </Button>,
+    <Button
+      type={'link'}
+      style={{ padding: 0 }}
+      key={'detail'}
+      onClick={() => {
+        AlarmLogModel.logVisible = true;
+        AlarmLogModel.current = record;
+      }}
+    >
+      <FileTextFilled />
+      处理记录
+    </Button>,
+  ];
+
+  const getAction = (actions: React.ReactNode[]) => {
+    return actions
+      .filter((item) => item)
+      .map((item: any) => {
+        return (
+          <div
+            className={classNames('card-button', {
+              disabled: item.disabled,
+            })}
+            key={item.key}
+          >
+            {item}
+          </div>
+        );
+      });
+  };
+
+  const defaultImage = require('/public/images/device-access.png');
+
   return (
     <div className="alarm-log-card">
       <SearchComponent<any>
@@ -119,130 +180,70 @@ const TabComponent = observer((props: Props) => {
       />
       <Card>
         {dataSource?.data.length > 0 ? (
-          <Row gutter={24} style={{ marginTop: 10 }}>
+          <Row gutter={[24, 24]} style={{ marginTop: 10 }}>
             {(dataSource?.data || []).map((item: any) => (
-              <Col key={item.id} span={24}>
-                <div className="alarm-log-item">
-                  <div className="alarm-log-title">
-                    <div
-                      className="alarm-log-level"
-                      style={{ backgroundColor: colorMap.get(item.level) }}
-                    >
-                      <div className="alarm-log-text">
-                        {AlarmLogModel.defaultLevel.find((i) => i.level === item.level)?.title ||
-                          item.level}
+              <Col key={item.id} span={12}>
+                <div className={classNames('iot-card')}>
+                  <div className={'card-warp'}>
+                    <div className={classNames('card-content')}>
+                      <div
+                        style={{ fontSize: 20, fontWeight: 700, marginBottom: 20 }}
+                        className="ellipsis"
+                      >
+                        <Tooltip title={item.alarmName}>
+                          <a>{item.alarmName}</a>
+                        </Tooltip>
                       </div>
-                    </div>
-                    <div className="alarm-log-title-text">
-                      <Tooltip placement="topLeft" title={item.alarmName}>
-                        {item.alarmName}
-                      </Tooltip>
-                    </div>
-                  </div>
-                  <div className="alarm-log-content">
-                    <div className="alarm-log-data">
-                      <div className="alarm-log-image">
-                        <img
-                          width={88}
-                          height={88}
-                          src={imgMap.get(props.type)}
-                          alt={''}
-                          style={{ marginRight: 20 }}
-                        />
-                        <div className="alarm-type">
-                          <div className="name">{titleMap.get(item.targetType)}</div>
-                          <div className="text">
-                            <Tooltip placement="topLeft" title={item.targetName}>
-                              {item.targetName}
-                            </Tooltip>
+                      <div className="alarm-log-context">
+                        <div className="context-left">
+                          <div className="context-img">
+                            <img width={70} height={70} src={defaultImage} alt={''} />
                           </div>
-                        </div>
-                      </div>
-                      <div className="alarm-log-right">
-                        <div className="alarm-log-time">
-                          <div className="log-title">最近告警时间</div>
-                          <div className="context">
-                            {moment(item.alarmTime).format('YYYY-MM-DD HH:mm:ss')}
+                          <div className="left-item">
+                            <div className="left-item-title">{titleMap.get(item.targetType)}</div>
+                            <div className="left-item-value ellipsis">
+                              <Tooltip placement="topLeft" title={item.targetName}>
+                                {item.targetName}
+                              </Tooltip>
+                            </div>
                           </div>
                         </div>
-                        <div
-                          className="alarm-log-time alarm-log-status"
-                          style={{ paddingLeft: 10 }}
-                        >
-                          <div className="log-title">状态</div>
-                          <div className="context">
-                            <Badge status={item.state.value === 'warning' ? 'error' : 'default'} />
-                            <span
-                              style={{
-                                color: item.state.value === 'warning' ? '#E50012' : 'black',
-                              }}
-                            >
-                              {item.state.text}
-                            </span>
+                        <div className="context-right">
+                          <div className="right-item">
+                            <div className="right-item-title">最近告警时间</div>
+                            <div className="right-item-value ellipsis">
+                              {moment(item.alarmTime).format('YYYY-MM-DD HH:mm:ss')}
+                            </div>
                           </div>
-                        </div>
-                      </div>
-                    </div>
-                    <div className="alarm-log-actions">
-                      <Space>
-                        {item.state.value === 'warning' && (
-                          <div
-                            className={classNames(
-                              permission.action ? 'alarm-log-action' : 'alarm-log-disabled',
-                            )}
-                          >
-                            <Tooltip title={permission.action ? '' : '暂无权限,请联系管理员'}>
-                              <Button
-                                type={'link'}
-                                disabled={!permission.action}
-                                onClick={() => {
-                                  AlarmLogModel.solveVisible = true;
-                                  AlarmLogModel.current = item;
+                          <div className="right-item">
+                            <div className="right-item-title">状态</div>
+                            <div className="right-item-value">
+                              <Badge
+                                status={item.state.value === 'warning' ? 'error' : 'default'}
+                              />
+                              <span
+                                style={{
+                                  color: item.state.value === 'warning' ? '#E50012' : 'black',
                                 }}
                               >
-                                <div className="btn">
-                                  <ToolFilled className="icon" />
-                                  <div>告警处理</div>
-                                </div>
-                              </Button>
-                            </Tooltip>
-                          </div>
-                        )}
-                        <div className="alarm-log-action">
-                          <Button
-                            type={'link'}
-                            onClick={() => {
-                              AlarmLogModel.current = item;
-                              const url = getMenuPathByParams(
-                                MENUS_CODE['rule-engine/Alarm/Log/Detail'],
-                                item.id,
-                              );
-                              history.push(url);
-                            }}
-                          >
-                            <div className="btn">
-                              <FileFilled className="icon" />
-                              <div>告警日志</div>
+                                {item.state.text}
+                              </span>
                             </div>
-                          </Button>
+                          </div>
                         </div>
-                        <div className="alarm-log-action">
-                          <Button
-                            type={'link'}
-                            onClick={() => {
-                              AlarmLogModel.logVisible = true;
-                              AlarmLogModel.current = item;
-                            }}
-                          >
-                            <div className="btn">
-                              <FileTextFilled className="icon" />
-                              <div>处理记录</div>
-                            </div>
-                          </Button>
+                      </div>
+                      <div
+                        className={'alarm-log-state'}
+                        style={{ backgroundColor: colorMap.get(item.level) }}
+                      >
+                        <div className={'card-state-content'}>
+                          {AlarmLogModel.defaultLevel.find((i) => i.level === item.level)?.title ||
+                            item.level}
                         </div>
-                      </Space>
+                      </div>
                     </div>
                   </div>
+                  <div className={'card-tools'}>{getAction(tools(item))}</div>
                 </div>
               </Col>
             ))}

+ 4 - 0
src/pages/rule-engine/Alarm/Log/index.tsx

@@ -11,6 +11,10 @@ export const service = new Service('alarm/record');
 const Log = observer(() => {
   const list = [
     {
+      key: 'all',
+      tab: '全部',
+    },
+    {
       key: 'product',
       tab: '产品',
     },

+ 1 - 1
src/pages/rule-engine/Alarm/Log/model.ts

@@ -12,7 +12,7 @@ export const AlarmLogModel = model<{
   }[];
   columns: ProColumns<AlarmLogHistoryItem>[];
 }>({
-  tab: 'product',
+  tab: 'all',
   current: {},
   solveVisible: false,
   logVisible: false,

+ 11 - 9
src/pages/system/Department/Assets/deivce/bind.tsx

@@ -1,9 +1,7 @@
 // 资产-产品分类-绑定
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
-import ProTable from '@jetlinks/pro-table';
 import { DeviceBadge, service } from './index';
 import { message, Modal } from 'antd';
-import { useParams } from 'umi';
 import Models from './model';
 import { useEffect, useRef, useState } from 'react';
 import { observer } from '@formily/react';
@@ -11,19 +9,21 @@ 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';
 
 interface Props {
   reload: () => void;
   visible: boolean;
   onCancel: () => void;
+  parentId: string;
 }
 
 const Bind = observer((props: Props) => {
   const intl = useIntl();
-  const param = useParams<{ id: string }>();
   const actionRef = useRef<ActionType>();
-  const [perVisible, setPerVisible] = useState(false);
   const [searchParam, setSearchParam] = useState({});
+  const saveRef = useRef<{ saveData: Function }>();
 
   const columns: ProColumns<DeviceItem>[] = [
     {
@@ -100,7 +100,7 @@ const Bind = observer((props: Props) => {
 
   const handleBind = () => {
     if (Models.bindKeys.length) {
-      setPerVisible(true);
+      saveRef.current?.saveData();
     } else {
       message.warn('请先勾选数据');
       // props.onCancel();
@@ -122,11 +122,11 @@ const Bind = observer((props: Props) => {
       title="绑定"
     >
       <PermissionModal
-        visible={perVisible}
         type="device"
         bindKeys={Models.bindKeys}
+        parentId={props.parentId}
+        ref={saveRef}
         onCancel={(type) => {
-          setPerVisible(false);
           if (type) {
             props.reload();
             props.onCancel();
@@ -146,7 +146,7 @@ const Bind = observer((props: Props) => {
               targets: [
                 {
                   type: 'org',
-                  id: param.id,
+                  id: props.parentId,
                 },
               ],
             },
@@ -163,11 +163,13 @@ const Bind = observer((props: Props) => {
         // }}
         target="department-assets-device"
       />
-      <ProTable<DeviceItem>
+      <ProTableCard<DeviceItem>
         actionRef={actionRef}
         columns={columns}
         rowKey="id"
         search={false}
+        gridColumn={2}
+        cardRender={(record) => <ExtraDeviceCard {...record} />}
         rowSelection={{
           selectedRowKeys: Models.bindKeys,
           onChange: (selectedRowKeys, selectedRows) => {

+ 87 - 15
src/pages/system/Department/Assets/deivce/index.tsx

@@ -1,10 +1,8 @@
-// 资产分配-产品分类
+// 资产分配-设备管理
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
-import ProTable from '@jetlinks/pro-table';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import { Badge, Button, message, Popconfirm, Tooltip } from 'antd';
-import { useRef, useState } from 'react';
-import { useParams } from 'umi';
+import { useEffect, useRef, useState } from 'react';
 import { observer } from '@formily/react';
 import type { DeviceItem } from '@/pages/system/Department/typings';
 import { DisconnectOutlined, PlusOutlined } from '@ant-design/icons';
@@ -12,6 +10,8 @@ import Models from './model';
 import Service from '@/pages/system/Department/Assets/service';
 import Bind from './bind';
 import SearchComponent from '@/components/SearchComponent';
+import { ExtraDeviceCard, handlePermissionsMap } from '@/components/ProTableCard/CardItems/device';
+import { ProTableCard } from '@/components';
 
 export const service = new Service<DeviceItem>('assets');
 
@@ -28,11 +28,10 @@ export const DeviceBadge = (props: DeviceBadgeProps) => {
   return <Badge status={STATUS[props.type]} text={props.text} />;
 };
 
-export default observer(() => {
+export default observer((props: { parentId: string }) => {
   const intl = useIntl();
   const actionRef = useRef<ActionType>();
 
-  const param = useParams<{ id: string }>();
   const [searchParam, setSearchParam] = useState({});
   /**
    * 解除资产绑定
@@ -43,7 +42,7 @@ export default observer(() => {
         .unBind('device', [
           {
             targetType: 'org',
-            targetId: param.id,
+            targetId: props.parentId,
             assetType: 'device',
             assetIdList: Models.unBindKeys,
           },
@@ -90,6 +89,14 @@ export default observer(() => {
       },
     },
     {
+      title: '资产权限',
+      dataIndex: 'grantedPermissions',
+      hideInSearch: true,
+      render: (_, row) => {
+        return handlePermissionsMap(row.grantedPermissions);
+      },
+    },
+    {
       title: intl.formatMessage({
         id: 'pages.device.instance.registrationTime',
         defaultMessage: '注册时间',
@@ -178,13 +185,48 @@ export default observer(() => {
     Models.bindKeys = [];
   };
 
+  const getData = (params: any, parentId: string) => {
+    return new Promise((resolve) => {
+      service.queryDeviceList2(params, parentId).subscribe((data) => {
+        resolve(data);
+      });
+    });
+  };
+
+  useEffect(() => {
+    setSearchParam({
+      terms: [
+        {
+          column: 'id',
+          termType: 'dim-assets',
+          value: {
+            assetType: 'device',
+            targets: [
+              {
+                type: 'org',
+                id: props.parentId,
+              },
+            ],
+          },
+        },
+      ],
+    });
+    actionRef.current?.reset?.();
+    //  初始化所有状态
+    Models.bindKeys = [];
+    Models.unBindKeys = [];
+  }, [props.parentId]);
+
   return (
     <>
-      <Bind
-        visible={Models.bind}
-        onCancel={closeModal}
-        reload={() => actionRef.current?.reload()}
-      />
+      {Models.bind && (
+        <Bind
+          visible={Models.bind}
+          onCancel={closeModal}
+          reload={() => actionRef.current?.reload()}
+          parentId={props.parentId}
+        />
+      )}
       <SearchComponent<DeviceItem>
         field={columns}
         defaultParam={[
@@ -196,7 +238,7 @@ export default observer(() => {
               targets: [
                 {
                   type: 'org',
-                  id: param.id,
+                  id: props.parentId,
                 },
               ],
             },
@@ -213,19 +255,48 @@ export default observer(() => {
         // }}
         target="department-assets-device"
       />
-      <ProTable<DeviceItem>
+      <ProTableCard<DeviceItem>
         actionRef={actionRef}
         columns={columns}
         rowKey="id"
         search={false}
         params={searchParam}
-        request={(params) => service.queryDeviceList(params)}
+        gridColumn={2}
+        request={async (params) => {
+          if (!props.parentId) {
+            return {
+              code: 200,
+              result: {
+                data: [],
+                pageIndex: 0,
+                pageSize: 0,
+                total: 0,
+              },
+              status: 200,
+            };
+          }
+          const resp: any = await getData(params, props.parentId);
+          return {
+            code: resp.status,
+            result: resp.result,
+            status: resp.status,
+          };
+        }}
         rowSelection={{
           selectedRowKeys: Models.unBindKeys,
           onChange: (selectedRowKeys, selectedRows) => {
             Models.unBindKeys = selectedRows.map((item) => item.id);
           },
         }}
+        cardRender={(record) => (
+          <ExtraDeviceCard
+            {...record}
+            onUnBind={(e) => {
+              e.stopPropagation();
+              singleUnBind(record.id);
+            }}
+          />
+        )}
         toolBarRender={() => [
           <Button
             onClick={() => {
@@ -234,6 +305,7 @@ export default observer(() => {
             icon={<PlusOutlined />}
             type="primary"
             key="bind"
+            disabled={!props.parentId}
           >
             {intl.formatMessage({
               id: 'pages.data.option.assets',

+ 36 - 25
src/pages/system/Department/Assets/index.tsx

@@ -4,35 +4,46 @@ import { useIntl } from '@@/plugin-locale/localeExports';
 import ProductCategory from './productCategory';
 import Product from './product';
 import Device from '@/pages/system/Department/Assets/deivce';
+import Member from '@/pages/system/Department/Member';
 
-// 资产类型
-const TabsArray = [
-  {
-    intlTitle: '1',
-    defaultMessage: '产品分类',
-    key: 'ProductCategory',
-    components: ProductCategory,
-  },
-  {
-    intlTitle: '2',
-    defaultMessage: '产品',
-    key: 'Product',
-    components: Product,
-  },
-  {
-    intlTitle: '3',
-    defaultMessage: '设备',
-    key: 'Device',
-    components: Device,
-  },
-];
+interface AssetsProps {
+  parentId: string;
+}
 
-const Assets = () => {
+const Assets = (props: AssetsProps) => {
   const intl = useIntl();
 
+  // 资产类型
+  const TabsArray = [
+    {
+      intlTitle: '1',
+      defaultMessage: '产品分类',
+      key: 'ProductCategory',
+      components: ProductCategory,
+    },
+    {
+      intlTitle: '2',
+      defaultMessage: '产品',
+      key: 'Product',
+      components: Product,
+    },
+    {
+      intlTitle: '3',
+      defaultMessage: '设备',
+      key: 'Device',
+      components: Device,
+    },
+    {
+      intlTitle: '4',
+      defaultMessage: '用户',
+      key: 'User',
+      components: Member,
+    },
+  ];
+
   return (
-    <div style={{ background: '#fff', padding: 12 }}>
-      <Tabs tabPosition="left" defaultActiveKey="ProductCategory">
+    <div>
+      <Tabs defaultActiveKey="ProductCategory">
         {TabsArray.map((item) => (
           <Tabs.TabPane
             tab={intl.formatMessage({
@@ -41,7 +52,7 @@ const Assets = () => {
             })}
             key={item.key}
           >
-            <item.components />
+            <item.components parentId={props.parentId} />
           </Tabs.TabPane>
         ))}
       </Tabs>

+ 16 - 23
src/pages/system/Department/Assets/permissionModal.tsx

@@ -1,19 +1,18 @@
 import { createForm } from '@formily/core';
 import { createSchemaField } from '@formily/react';
 import { Form, FormItem, Checkbox } from '@formily/antd';
-import { message, Modal } from 'antd';
-import { useIntl } from '@@/plugin-locale/localeExports';
+import { message } from 'antd';
 import type { ISchema } from '@formily/json-schema';
 import type { ModalProps } from 'antd/lib/modal/Modal';
-import { useParams } from 'umi';
 import Service from './service';
+import { forwardRef, useImperativeHandle } from 'react';
 
 type PermissionType = 'device' | 'product' | 'deviceCategory';
 
 export interface PerModalProps extends Omit<ModalProps, 'onOk' | 'onCancel'> {
   type: PermissionType;
+  parentId: string;
   bindKeys: string[];
-  visible: boolean;
   /**
    * Model关闭事件
    * @param type 是否为请求接口后关闭,用于外部table刷新数据
@@ -23,10 +22,7 @@ export interface PerModalProps extends Omit<ModalProps, 'onOk' | 'onCancel'> {
 
 const service = new Service('assets');
 
-export default (props: PerModalProps) => {
-  const intl = useIntl();
-  const params = useParams<{ id: string }>();
-
+const Permission = forwardRef((props: PerModalProps, ref) => {
   const SchemaField = createSchemaField({
     components: {
       Form,
@@ -56,7 +52,7 @@ export default (props: PerModalProps) => {
       .bind(props.type, [
         {
           targetType: 'org',
-          targetId: params.id,
+          targetId: props.parentId,
           assetType: props.type,
           assetIdList: props.bindKeys,
           permission: formData.permission,
@@ -71,6 +67,10 @@ export default (props: PerModalProps) => {
       });
   };
 
+  useImperativeHandle(ref, () => ({
+    saveData,
+  }));
+
   const schema: ISchema = {
     type: 'object',
     properties: {
@@ -84,26 +84,19 @@ export default (props: PerModalProps) => {
           { label: '编辑', value: 'save' },
           { label: '删除', value: 'delete' },
         ],
+        required: true,
         'x-value': ['read'],
       },
     },
   };
 
   return (
-    <Modal
-      title={intl.formatMessage({
-        id: `pages.data.option.`,
-        defaultMessage: '资产权限',
-      })}
-      visible={props.visible}
-      onOk={saveData}
-      onCancel={() => {
-        modalClose(false);
-      }}
-    >
-      <Form form={form} labelCol={5} wrapperCol={16}>
+    <div style={{ borderBottom: '1px solid #f0f0f0' }}>
+      <Form form={form}>
         <SchemaField schema={schema} />
       </Form>
-    </Modal>
+    </div>
   );
-};
+});
+
+export default Permission;

+ 12 - 10
src/pages/system/Department/Assets/product/bind.tsx

@@ -1,9 +1,7 @@
 // 资产-产品分类-绑定
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
-import ProTable from '@jetlinks/pro-table';
 import { service } from './index';
 import { message, Modal } from 'antd';
-import { useParams } from 'umi';
 import Models from './model';
 import { useEffect, useRef, useState } from 'react';
 import { observer } from '@formily/react';
@@ -11,19 +9,21 @@ 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';
 
 interface Props {
   reload: () => void;
   visible: boolean;
   onCancel: () => void;
+  parentId: string;
 }
 
 const Bind = observer((props: Props) => {
   const intl = useIntl();
-  const param = useParams<{ id: string }>();
   const actionRef = useRef<ActionType>();
-  const [perVisible, setPerVisible] = useState(false);
   const [searchParam, setSearchParam] = useState({});
+  const saveRef = useRef<{ saveData: Function }>();
 
   const columns: ProColumns<ProductItem>[] = [
     {
@@ -52,8 +52,8 @@ const Bind = observer((props: Props) => {
   ];
 
   const handleBind = () => {
-    if (Models.bindKeys.length) {
-      setPerVisible(true);
+    if (Models.bindKeys.length && saveRef.current) {
+      saveRef.current?.saveData();
     } else {
       message.warn('请先勾选数据');
       // props.onCancel();
@@ -75,11 +75,11 @@ const Bind = observer((props: Props) => {
       title="绑定"
     >
       <PermissionModal
-        visible={perVisible}
         type="product"
+        parentId={props.parentId}
         bindKeys={Models.bindKeys}
+        ref={saveRef}
         onCancel={(type) => {
-          setPerVisible(false);
           if (type) {
             props.reload();
             props.onCancel();
@@ -99,7 +99,7 @@ const Bind = observer((props: Props) => {
               targets: [
                 {
                   type: 'org',
-                  id: param.id,
+                  id: props.parentId,
                 },
               ],
             },
@@ -116,11 +116,12 @@ const Bind = observer((props: Props) => {
         // }}
         target="department-assets-product"
       />
-      <ProTable<ProductItem>
+      <ProTableCard<ProductItem>
         actionRef={actionRef}
         columns={columns}
         rowKey="id"
         search={false}
+        gridColumn={2}
         rowSelection={{
           selectedRowKeys: Models.bindKeys,
           onChange: (selectedRowKeys, selectedRows) => {
@@ -129,6 +130,7 @@ const Bind = observer((props: Props) => {
         }}
         request={(params) => service.queryProductList(params)}
         params={searchParam}
+        cardRender={(record) => <ExtraProductCard {...record} />}
       />
     </Modal>
   );

+ 89 - 14
src/pages/system/Department/Assets/product/index.tsx

@@ -1,10 +1,8 @@
 // 资产分配-产品分类
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
-import ProTable from '@jetlinks/pro-table';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import { Button, message, Popconfirm, Tooltip } from 'antd';
-import { useRef, useState } from 'react';
-import { useParams } from 'umi';
+import { useEffect, useRef, useState } from 'react';
 import { observer } from '@formily/react';
 import type { ProductItem } from '@/pages/system/Department/typings';
 import { DisconnectOutlined, PlusOutlined } from '@ant-design/icons';
@@ -12,14 +10,18 @@ import Service from '@/pages/system/Department/Assets/service';
 import Models from './model';
 import Bind from './bind';
 import SearchComponent from '@/components/SearchComponent';
+import {
+  ExtraProductCard,
+  handlePermissionsMap,
+} from '@/components/ProTableCard/CardItems/product';
+import { ProTableCard } from '@/components';
 
 export const service = new Service<ProductItem>('assets');
 
-export default observer(() => {
+export default observer((props: { parentId: string }) => {
   const intl = useIntl();
   const actionRef = useRef<ActionType>();
 
-  const param = useParams<{ id: string }>();
   const [searchParam, setSearchParam] = useState({});
 
   /**
@@ -31,7 +33,7 @@ export default observer(() => {
         .unBind('product', [
           {
             targetType: 'org',
-            targetId: param.id,
+            targetId: props.parentId,
             assetType: 'product',
             assetIdList: Models.unBindKeys,
           },
@@ -71,6 +73,14 @@ export default observer(() => {
       },
     },
     {
+      title: '资产权限',
+      dataIndex: 'grantedPermissions',
+      hideInSearch: true,
+      render: (_, row) => {
+        return handlePermissionsMap(row.grantedPermissions);
+      },
+    },
+    {
       title: intl.formatMessage({
         id: 'pages.system.description',
         defaultMessage: '说明',
@@ -117,13 +127,48 @@ export default observer(() => {
     Models.bindKeys = [];
   };
 
+  useEffect(() => {
+    setSearchParam({
+      terms: [
+        {
+          column: 'id',
+          termType: 'dim-assets',
+          value: {
+            assetType: 'product',
+            targets: [
+              {
+                type: 'org',
+                id: props.parentId,
+              },
+            ],
+          },
+        },
+      ],
+    });
+    actionRef.current?.reload();
+    //  初始化所有状态
+    Models.bindKeys = [];
+    Models.unBindKeys = [];
+  }, [props.parentId]);
+
+  const getData = (params: any, parentId: string) => {
+    return new Promise((resolve) => {
+      service.queryProductList2(params, parentId).subscribe((data) => {
+        resolve(data);
+      });
+    });
+  };
+
   return (
     <>
-      <Bind
-        visible={Models.bind}
-        onCancel={closeModal}
-        reload={() => actionRef.current?.reload()}
-      />
+      {Models.bind && (
+        <Bind
+          visible={Models.bind}
+          onCancel={closeModal}
+          reload={() => actionRef.current?.reload()}
+          parentId={props.parentId}
+        />
+      )}
       <SearchComponent<ProductItem>
         field={columns}
         defaultParam={[
@@ -135,7 +180,7 @@ export default observer(() => {
               targets: [
                 {
                   type: 'org',
-                  id: param.id,
+                  id: props.parentId,
                 },
               ],
             },
@@ -152,19 +197,48 @@ export default observer(() => {
         // }}
         target="department-assets-product"
       />
-      <ProTable<ProductItem>
+      <ProTableCard<ProductItem>
         actionRef={actionRef}
         columns={columns}
         rowKey="id"
         search={false}
+        gridColumn={2}
         params={searchParam}
-        request={(params) => service.queryProductList(params)}
+        request={async (params) => {
+          if (!props.parentId) {
+            return {
+              code: 200,
+              result: {
+                data: [],
+                pageIndex: 0,
+                pageSize: 0,
+                total: 0,
+              },
+              status: 200,
+            };
+          }
+          const resp: any = await getData(params, props.parentId);
+          return {
+            code: resp.status,
+            result: resp.result,
+            status: resp.status,
+          };
+        }}
         rowSelection={{
           selectedRowKeys: Models.unBindKeys,
           onChange: (selectedRowKeys, selectedRows) => {
+            console.log(selectedRows);
             Models.unBindKeys = selectedRows.map((item) => item.id);
           },
         }}
+        cardRender={(record) => (
+          <ExtraProductCard
+            {...record}
+            onUnBind={() => {
+              singleUnBind(record.id);
+            }}
+          />
+        )}
         toolBarRender={() => [
           <Button
             onClick={() => {
@@ -173,6 +247,7 @@ export default observer(() => {
             icon={<PlusOutlined />}
             type="primary"
             key="bind"
+            disabled={!props.parentId}
           >
             {intl.formatMessage({
               id: 'pages.data.option.assets',

+ 7 - 8
src/pages/system/Department/Assets/productCategory/bind.tsx

@@ -3,7 +3,6 @@ import type { ActionType, ProColumns } from '@jetlinks/pro-table';
 import ProTable from '@jetlinks/pro-table';
 import { getTableKeys, service } from './index';
 import { Button, message, Modal, Space } from 'antd';
-import { useParams } from 'umi';
 import Models from './model';
 import { useEffect, useRef, useState } from 'react';
 import { observer } from '@formily/react';
@@ -17,14 +16,14 @@ interface Props {
   reload: () => void;
   visible: boolean;
   onCancel: () => void;
+  parentId: string;
 }
 
 const Bind = observer((props: Props) => {
   const intl = useIntl();
-  const param = useParams<{ id: string }>();
   const actionRef = useRef<ActionType>();
-  const [perVisible, setPerVisible] = useState(false);
   const [searchParam, setSearchParam] = useState({});
+  const saveRef = useRef<{ saveData: Function }>();
 
   const columns: ProColumns<ProductCategoryItem>[] = [
     {
@@ -52,8 +51,8 @@ const Bind = observer((props: Props) => {
   ];
 
   const handleBind = () => {
-    if (Models.bindKeys.length) {
-      setPerVisible(true);
+    if (Models.bindKeys.length && saveRef.current) {
+      saveRef.current.saveData();
     } else {
       message.warn('请先勾选数据');
       // props.onCancel();
@@ -75,11 +74,11 @@ const Bind = observer((props: Props) => {
       title="绑定"
     >
       <PermissionModal
-        visible={perVisible}
         type="deviceCategory"
         bindKeys={Models.bindKeys}
+        ref={saveRef}
+        parentId={props.parentId}
         onCancel={(type) => {
-          setPerVisible(false);
           if (type) {
             props.reload();
             props.onCancel();
@@ -99,7 +98,7 @@ const Bind = observer((props: Props) => {
               targets: [
                 {
                   type: 'org',
-                  id: param.id,
+                  id: props.parentId,
                 },
               ],
             },

+ 50 - 11
src/pages/system/Department/Assets/productCategory/index.tsx

@@ -3,8 +3,7 @@ import type { ActionType, ProColumns } from '@jetlinks/pro-table';
 import ProTable from '@jetlinks/pro-table';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import { Button, message, Popconfirm, Space, Tooltip } from 'antd';
-import { useRef, useState } from 'react';
-import { useParams } from 'umi';
+import { useEffect, useRef, useState } from 'react';
 import { observer } from '@formily/react';
 import type { ProductCategoryItem } from '@/pages/system/Department/typings';
 import { DisconnectOutlined, PlusOutlined } from '@ant-design/icons';
@@ -29,10 +28,9 @@ export const getTableKeys = (rows: ProductCategoryItem[]): string[] => {
   return keys;
 };
 
-export default observer(() => {
+export default observer((props: { parentId: string }) => {
   const intl = useIntl();
   const actionRef = useRef<ActionType>();
-  const param = useParams<{ id: string }>();
   const [searchParam, setSearchParam] = useState({});
 
   /**
@@ -44,7 +42,7 @@ export default observer(() => {
         .unBind('deviceCategory', [
           {
             targetType: 'org',
-            targetId: param.id,
+            targetId: props.parentId,
             assetType: 'deviceCategory',
             assetIdList: Models.unBindKeys,
           },
@@ -137,13 +135,41 @@ export default observer(() => {
     Models.bindKeys = [];
   };
 
+  useEffect(() => {
+    setSearchParam({
+      terms: [
+        {
+          column: 'id',
+          termType: 'dim-assets',
+          value: {
+            assetType: 'deviceCategory',
+            targets: [
+              {
+                type: 'org',
+                id: props.parentId,
+              },
+            ],
+          },
+        },
+      ],
+    });
+    actionRef.current?.reset?.();
+    //  初始化所有状态
+    Models.bindKeys = [];
+    Models.unBindKeys = [];
+    console.log(props.parentId);
+  }, [props.parentId]);
+
   return (
     <>
-      <Bind
-        visible={Models.bind}
-        onCancel={closeModal}
-        reload={() => actionRef.current?.reload()}
-      />
+      {Models.bind && (
+        <Bind
+          visible={Models.bind}
+          onCancel={closeModal}
+          reload={() => actionRef.current?.reload()}
+          parentId={props.parentId}
+        />
+      )}
       <SearchComponent<ProductCategoryItem>
         field={columns}
         defaultParam={[
@@ -155,7 +181,7 @@ export default observer(() => {
               targets: [
                 {
                   type: 'org',
-                  id: param.id,
+                  id: props.parentId,
                 },
               ],
             },
@@ -179,6 +205,18 @@ export default observer(() => {
         search={false}
         rowKey="id"
         request={async (params) => {
+          if (!props.parentId) {
+            return {
+              code: 200,
+              result: {
+                data: [],
+                pageIndex: 0,
+                pageSize: 0,
+                total: 0,
+              },
+              status: 200,
+            };
+          }
           const response = await service.queryProductCategoryList(params);
           return {
             code: response.message,
@@ -240,6 +278,7 @@ export default observer(() => {
             }}
             icon={<PlusOutlined />}
             type="primary"
+            disabled={!props.parentId}
             key="bind"
           >
             {intl.formatMessage({

+ 57 - 1
src/pages/system/Department/Assets/service.ts

@@ -2,7 +2,7 @@ import BaseService from '@/utils/BaseService';
 import { request } from '@@/plugin-request/request';
 import SystemConst from '@/utils/const';
 import { defer, from } from 'rxjs';
-import { filter, map } from 'rxjs/operators';
+import { filter, map, mergeMap } from 'rxjs/operators';
 
 class Service<T> extends BaseService<T> {
   // 资产绑定
@@ -29,6 +29,7 @@ class Service<T> extends BaseService<T> {
       },
     });
   };
+
   // 资产-设备
   queryDeviceList = (params: any) => {
     return request<T>(`${SystemConst.API_BASE}/device/instance/_query`, {
@@ -36,6 +37,34 @@ class Service<T> extends BaseService<T> {
       data: params,
     });
   };
+
+  queryDeviceList2 = (params: any, parentId: string) =>
+    from(
+      request(`${SystemConst.API_BASE}/device/instance/_query`, { method: 'POST', data: params }),
+    ).pipe(
+      filter((item) => item.status === 200),
+      mergeMap((result) => {
+        const ids = result?.result?.data?.map((item: any) => item.id) || [];
+        return from(
+          request(`${SystemConst.API_BASE}/assets/bindings/device/org/${parentId}/_query`, {
+            method: 'POST',
+            data: ids,
+          }),
+        ).pipe(
+          filter((item) => item.status === 200),
+          map((item: any) => item.result || []),
+          map((item: any) => {
+            result.result.data = result.result.data.map((a: any) => {
+              a.grantedPermissions =
+                item.find((b: any) => b.assetId === a.id)?.grantedPermissions || [];
+              return a;
+            });
+            return result;
+          }),
+        );
+      }),
+    );
+
   // 资产-产品
   queryProductList = (params: any) => {
     return request<T>(`${SystemConst.API_BASE}/device-product/_query`, {
@@ -43,6 +72,33 @@ class Service<T> extends BaseService<T> {
       data: params,
     });
   };
+
+  queryProductList2 = (params: any, parentId: string) =>
+    from(
+      request(`${SystemConst.API_BASE}/device-product/_query`, { method: 'POST', data: params }),
+    ).pipe(
+      filter((item) => item.status === 200),
+      mergeMap((result) => {
+        const ids = result?.result?.data?.map((item: any) => item.id) || [];
+        return from(
+          request(`${SystemConst.API_BASE}/assets/bindings/device-product/org/${parentId}/_query`, {
+            method: 'POST',
+            data: ids,
+          }),
+        ).pipe(
+          filter((item) => item.status === 200),
+          map((item: any) => item.result || []),
+          map((item: any) => {
+            result.result.data = result.result.data.map((a: any) => {
+              a.grantedPermissions =
+                item.find((b: any) => b.assetId === a.id)?.grantedPermissions || [];
+              return a;
+            });
+            return result;
+          }),
+        );
+      }),
+    );
 }
 
 export default Service;

+ 3 - 4
src/pages/system/Department/Member/bind.tsx

@@ -3,7 +3,6 @@ import type { ActionType, ProColumns } from '@jetlinks/pro-table';
 import ProTable from '@jetlinks/pro-table';
 import { service } from '@/pages/system/Department/Member';
 import { message, Modal } from 'antd';
-import { useParams } from 'umi';
 import MemberModel from '@/pages/system/Department/Member/model';
 import { observer } from '@formily/react';
 import { useEffect, useRef, useState } from 'react';
@@ -14,11 +13,11 @@ interface Props {
   reload: () => void;
   visible: boolean;
   onCancel: () => void;
+  parentId: string;
 }
 
 const Bind = observer((props: Props) => {
   const intl = useIntl();
-  const param = useParams<{ id: string }>();
   const [searchParam, setSearchParam] = useState({});
   const actionRef = useRef<ActionType>();
 
@@ -53,7 +52,7 @@ const Bind = observer((props: Props) => {
 
   const handleBind = () => {
     if (MemberModel.bindUsers.length) {
-      service.handleUser(param.id, MemberModel.bindUsers, 'bind').subscribe({
+      service.handleUser(props.parentId, MemberModel.bindUsers, 'bind').subscribe({
         next: () => message.success('操作成功'),
         error: () => message.error('操作失败'),
         complete: () => {
@@ -80,7 +79,7 @@ const Bind = observer((props: Props) => {
         // pattern={'simple'}
         enableSave={false}
         field={columns}
-        defaultParam={[{ column: 'id$in-dimension$org$not', value: param.id }]}
+        defaultParam={[{ column: 'id$in-dimension$org$not', value: props.parentId }]}
         onSearch={async (data) => {
           actionRef.current?.reset?.();
           setSearchParam(data);

+ 39 - 12
src/pages/system/Department/Member/index.tsx

@@ -3,8 +3,7 @@ import type { ActionType, ProColumns } from '@jetlinks/pro-table';
 import ProTable from '@jetlinks/pro-table';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import { Badge, Button, message, Popconfirm, Tooltip } from 'antd';
-import { useRef, useState } from 'react';
-import { useParams } from 'umi';
+import { useEffect, useRef, useState } from 'react';
 import { observer } from '@formily/react';
 import MemberModel from '@/pages/system/Department/Member/model';
 import type { MemberItem } from '@/pages/system/Department/typings';
@@ -12,19 +11,19 @@ import Service from '@/pages/system/Department/Member/service';
 import { DisconnectOutlined, PlusOutlined } from '@ant-design/icons';
 import Bind from './bind';
 import SearchComponent from '@/components/SearchComponent';
+import Models from '@/pages/system/Department/Assets/productCategory/model';
 
 export const service = new Service('tenant');
 
-const Member = observer(() => {
+const Member = observer((props: { parentId: string }) => {
   const intl = useIntl();
   const actionRef = useRef<ActionType>();
 
-  const param = useParams<{ id: string }>();
   const [searchParam, setSearchParam] = useState({});
 
   const handleUnBind = () => {
     if (MemberModel.unBindUsers.length) {
-      service.handleUser(param.id, MemberModel.unBindUsers, 'unbind').subscribe({
+      service.handleUser(props.parentId, MemberModel.unBindUsers, 'unbind').subscribe({
         next: () => message.success('操作成功'),
         error: () => message.error('操作失败'),
         complete: () => {
@@ -149,17 +148,30 @@ const Member = observer(() => {
     MemberModel.bind = false;
   };
 
+  useEffect(() => {
+    setSearchParam({
+      terms: [{ column: 'id$in-dimension$org', value: props.parentId }],
+    });
+    actionRef.current?.reset?.();
+    //  初始化所有状态
+    Models.bindKeys = [];
+    Models.unBindKeys = [];
+  }, [props.parentId]);
+
   return (
     <>
-      <Bind
-        visible={MemberModel.bind}
-        onCancel={closeModal}
-        reload={() => actionRef.current?.reload()}
-      />
+      {MemberModel.bind && (
+        <Bind
+          visible={MemberModel.bind}
+          onCancel={closeModal}
+          reload={() => actionRef.current?.reload()}
+          parentId={props.parentId}
+        />
+      )}
       <SearchComponent<MemberItem>
         // pattern={'simple'}
         field={columns}
-        defaultParam={[{ column: 'id$in-dimension$org', value: param.id }]}
+        defaultParam={[{ column: 'id$in-dimension$org', value: props.parentId }]}
         onSearch={async (data) => {
           actionRef.current?.reset?.();
           setSearchParam(data);
@@ -176,7 +188,21 @@ const Member = observer(() => {
         columns={columns}
         search={false}
         rowKey="id"
-        request={(params) => service.queryUser(params)}
+        request={(params) => {
+          if (!props.parentId) {
+            return {
+              code: 200,
+              result: {
+                data: [],
+                pageIndex: 0,
+                pageSize: 0,
+                total: 0,
+              },
+              status: 200,
+            };
+          }
+          return service.queryUser(params);
+        }}
         rowSelection={{
           selectedRowKeys: MemberModel.unBindUsers,
           onChange: (selectedRowKeys, selectedRows) => {
@@ -192,6 +218,7 @@ const Member = observer(() => {
             icon={<PlusOutlined />}
             type="primary"
             key="bind"
+            disabled={!props.parentId}
           >
             {intl.formatMessage({
               id: 'pages.system.role.option.bindUser',

+ 3 - 0
src/pages/system/Department/Tree/index.tsx

@@ -0,0 +1,3 @@
+import Tree from './tree';
+
+export default Tree;

+ 331 - 0
src/pages/system/Department/Tree/tree.tsx

@@ -0,0 +1,331 @@
+import { Button, Input, message, Tree } from 'antd';
+import {
+  DeleteOutlined,
+  EditOutlined,
+  LoadingOutlined,
+  PlusCircleOutlined,
+  SearchOutlined,
+} from '@ant-design/icons';
+import { useEffect, useRef, useState } from 'react';
+import { service } from '@/pages/system/Department';
+import { Empty, PermissionButton } from '@/components';
+import { useIntl } from 'umi';
+import { debounce } from 'lodash';
+import Save from '../save';
+import { ISchema } from '@formily/json-schema';
+import { useLocation } from 'umi';
+import { DepartmentItem } from '@/pages/system/Department/typings';
+
+interface TreeProps {
+  onSelect: (id: string) => void;
+}
+
+export const getSortIndex = (data: DepartmentItem[], pId?: string): number => {
+  let sortIndex = 0;
+  if (data.length) {
+    if (!pId) {
+      return data.sort((a, b) => b.sortIndex - a.sortIndex)[0].sortIndex + 1;
+    }
+    data.some((department) => {
+      if (department.id === pId && department.children) {
+        const sortArray = department.children.sort((a, b) => b.sortIndex - a.sortIndex);
+        sortIndex = sortArray[0].sortIndex + 1;
+        return true;
+      } else if (department.children) {
+        sortIndex = getSortIndex(department.children, pId);
+        return !!sortIndex;
+      }
+      return false;
+    });
+  }
+  return sortIndex;
+};
+
+export default (props: TreeProps) => {
+  const intl = useIntl();
+  const [treeData, setTreeData] = useState<undefined | any[]>(undefined);
+  const [loading, setLoading] = useState(false);
+  const [keys, setKeys] = useState<any[]>([]);
+  const [visible, setVisible] = useState(false);
+  const [data, setData] = useState<any>();
+  const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
+  const searchKey = useRef('');
+
+  const location = useLocation();
+
+  const { permission } = PermissionButton.usePermission('system/Department');
+
+  const queryTreeData = async () => {
+    setKeys([]);
+    const terms: Record<string, any> = {};
+    if (searchKey.current) {
+      terms.terms = [{ column: 'name$LIKE', value: `%${searchKey.current}%` }];
+    }
+    setLoading(true);
+    const resp = await service.queryOrgThree({
+      paging: false,
+      sorts: [{ name: 'sortIndex', order: 'asc' }],
+      ...terms,
+    });
+    setLoading(false);
+
+    if (resp.status === 200) {
+      setTreeData(resp.result);
+    }
+  };
+
+  const deleteItem = async (id: string) => {
+    const response: any = await service.remove(id);
+    if (response.status === 200) {
+      message.success(
+        intl.formatMessage({
+          id: 'pages.data.option.success',
+          defaultMessage: '操作成功!',
+        }),
+      );
+      queryTreeData();
+    }
+  };
+
+  const onSearchChange = (e: any) => {
+    searchKey.current = e.target.value;
+    queryTreeData();
+  };
+
+  const schema: ISchema = {
+    type: 'object',
+    properties: {
+      parentId: {
+        type: 'string',
+        title: '上级部门',
+        'x-decorator': 'FormItem',
+        'x-component': 'TreeSelect',
+        'x-component-props': {
+          fieldNames: {
+            label: 'name',
+            value: 'id',
+          },
+          placeholder: '请选择上级部门',
+        },
+        enum: treeData,
+      },
+      name: {
+        type: 'string',
+        title: intl.formatMessage({
+          id: 'pages.table.name',
+          defaultMessage: '名称',
+        }),
+        required: true,
+        'x-decorator': 'FormItem',
+        'x-component': 'Input',
+        'x-component-props': {
+          placeholder: '请输入名称',
+        },
+        'x-validator': [
+          {
+            max: 64,
+            message: '最多可输入64个字符',
+          },
+          {
+            required: true,
+            message: '请输入名称',
+          },
+        ],
+      },
+      sortIndex: {
+        type: 'string',
+        title: intl.formatMessage({
+          id: 'pages.device.instanceDetail.detail.sort',
+          defaultMessage: '排序',
+        }),
+        required: true,
+        'x-decorator': 'FormItem',
+        'x-component': 'NumberPicker',
+        'x-component-props': {
+          placeholder: '请输入排序',
+        },
+        'x-validator': [
+          {
+            required: true,
+            message: '请输入排序',
+          },
+          {
+            pattern: /^[0-9]*[1-9][0-9]*$/,
+            message: '请输入大于0的整数',
+          },
+        ],
+      },
+    },
+  };
+
+  useEffect(() => {
+    if ((location as any).query?.save === 'true') {
+      setData({ sortIndex: treeData && treeData.length + 1 });
+      setVisible(true);
+    }
+  }, [location]);
+
+  useEffect(() => {
+    queryTreeData();
+  }, []);
+
+  useEffect(() => {
+    if (keys.length) {
+      props.onSelect(keys[0]);
+    }
+  }, [keys]);
+
+  return (
+    <div className={'left-tree-content border-left'}>
+      {loading && (
+        <div className={'left-tree-loading'}>
+          <LoadingOutlined />
+        </div>
+      )}
+      <Input
+        placeholder={'请输入部门名称'}
+        className={'left-tree-search'}
+        suffix={<SearchOutlined />}
+        onChange={debounce(onSearchChange, 500)}
+      />
+      <Button
+        style={{ width: '100%', margin: '24px 0' }}
+        type={'primary'}
+        onClick={() => {
+          setData({ sortIndex: treeData && treeData.length + 1 });
+          setVisible(true);
+        }}
+      >
+        新增
+      </Button>
+      {treeData ? (
+        <div className={'left-tree-body'}>
+          <Tree
+            fieldNames={{
+              title: 'name',
+              key: 'id',
+            }}
+            blockNode={true}
+            treeData={treeData}
+            selectedKeys={keys}
+            onSelect={(_keys: any[]) => {
+              if (_keys && _keys.length) {
+                setKeys(_keys);
+              }
+            }}
+            expandedKeys={expandedKeys}
+            onExpand={(_keys: any[]) => {
+              setExpandedKeys(_keys);
+            }}
+            titleRender={(nodeData: any) => {
+              return (
+                <div>
+                  <span>{nodeData.name}</span>
+                  <span>
+                    <PermissionButton
+                      key="editable"
+                      tooltip={{
+                        title: intl.formatMessage({
+                          id: 'pages.data.option.edit',
+                          defaultMessage: '编辑',
+                        }),
+                      }}
+                      isPermission={permission.update}
+                      style={{ padding: '0 0 0 6px' }}
+                      type="link"
+                      onClick={(e) => {
+                        e.stopPropagation();
+                        setData({
+                          ...nodeData,
+                        });
+                        setVisible(true);
+                      }}
+                    >
+                      <EditOutlined />
+                    </PermissionButton>
+                    <PermissionButton
+                      key={'addChildren'}
+                      style={{ padding: '0 0 0 6px' }}
+                      tooltip={{
+                        title: intl.formatMessage({
+                          id: 'pages.system.department.option.add',
+                          defaultMessage: '新增子部门',
+                        }),
+                      }}
+                      type="link"
+                      isPermission={permission.add}
+                      onClick={(e) => {
+                        e.stopPropagation();
+                        setData({
+                          parentId: nodeData.id,
+                          sortIndex: nodeData.children ? nodeData.children.length + 1 : 1,
+                        });
+                        setVisible(true);
+                      }}
+                    >
+                      <PlusCircleOutlined />
+                    </PermissionButton>
+                    <PermissionButton
+                      type="link"
+                      key="delete"
+                      style={{ padding: '0 0 0 6px' }}
+                      popConfirm={{
+                        title: intl.formatMessage({
+                          id: 'pages.system.role.option.delete',
+                          defaultMessage: '确定要删除吗',
+                        }),
+                        onConfirm(e) {
+                          e?.stopPropagation();
+                          deleteItem(nodeData.id);
+                        },
+                      }}
+                      onClick={(e) => {
+                        e.stopPropagation();
+                      }}
+                      tooltip={{
+                        title: intl.formatMessage({
+                          id: 'pages.data.option.delete',
+                          defaultMessage: '删除',
+                        }),
+                      }}
+                      isPermission={permission.delete}
+                    >
+                      <DeleteOutlined />
+                    </PermissionButton>
+                  </span>
+                </div>
+              );
+            }}
+          />
+        </div>
+      ) : (
+        <div style={{ height: 200 }}>
+          <Empty />
+        </div>
+      )}
+      <Save
+        visible={visible}
+        title={
+          data && data.parentId
+            ? intl.formatMessage({
+                id: 'pages.system.department.option.add',
+              })
+            : undefined
+        }
+        service={service}
+        onCancel={() => {
+          setVisible(false);
+          setData(undefined);
+        }}
+        reload={async (pId) => {
+          await queryTreeData();
+          if (pId && !expandedKeys.includes(pId)) {
+            setExpandedKeys([...expandedKeys, pId]);
+          }
+        }}
+        data={data}
+        schema={schema}
+      />
+    </div>
+  );
+};

+ 52 - 0
src/pages/system/Department/index.less

@@ -0,0 +1,52 @@
+.department {
+  .ant-card-body {
+    height: 100%;
+  }
+
+  .department-warp {
+    display: flex;
+    height: 100%;
+
+    .department-left {
+      display: flex;
+      flex-basis: 300px;
+      height: 100%;
+
+      .border-left {
+        padding-right: 24px;
+        border-right: 1px solid #f0f0f0;
+      }
+
+      .left-tree-content {
+        position: relative;
+        display: flex;
+        flex-direction: column;
+        width: 100%;
+
+        .left-tree-loading {
+          position: absolute;
+          top: 0;
+          left: 0;
+          z-index: 2;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          width: 100%;
+          height: 100%;
+          font-size: 30px;
+          background-color: rgba(#fff, 0.1);
+        }
+      }
+
+      .left-tree-body {
+        flex: 1 1 auto;
+        overflow-y: auto;
+      }
+    }
+
+    .department-right {
+      flex: 1 1 auto;
+      padding-left: 24px;
+    }
+  }
+}

+ 122 - 370
src/pages/system/Department/index.tsx

@@ -1,28 +1,15 @@
 // 部门管理
 import { PageContainer } from '@ant-design/pro-layout';
-import type { ActionType, ProColumns } from '@jetlinks/pro-table';
-import ProTable from '@jetlinks/pro-table';
-import * as React from 'react';
-import { useEffect, useRef, useState } from 'react';
-import { history, useIntl, useLocation } from 'umi';
-import { message } from 'antd';
-import {
-  DeleteOutlined,
-  EditOutlined,
-  MedicineBoxOutlined,
-  PlusCircleOutlined,
-  PlusOutlined,
-  TeamOutlined,
-} from '@ant-design/icons';
+import { useEffect, useState } from 'react';
+import { Card } from 'antd';
 import Service from '@/pages/system/Department/service';
-import type { ISchema } from '@formily/json-schema';
 import type { DepartmentItem } from '@/pages/system/Department/typings';
 import { observer } from '@formily/react';
 import { model } from '@formily/reactive';
-import Save from './save';
-import SearchComponent from '@/components/SearchComponent';
-import { getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
-import { PermissionButton } from '@/components';
+import { getDomFullHeight } from '@/utils/util';
+import Assets from './Assets';
+import Tree from './Tree';
+import './style';
 
 export const service = new Service('organization');
 
@@ -37,365 +24,130 @@ export const State = model<ModelType>({
   parentId: undefined,
 });
 
-export const getSortIndex = (data: DepartmentItem[], pId?: string): number => {
-  let sortIndex = 0;
-  if (data.length) {
-    if (!pId) {
-      return data.sort((a, b) => b.sortIndex - a.sortIndex)[0].sortIndex + 1;
-    }
-    data.some((department) => {
-      if (department.id === pId && department.children) {
-        const sortArray = department.children.sort((a, b) => b.sortIndex - a.sortIndex);
-        sortIndex = sortArray[0].sortIndex + 1;
-        return true;
-      } else if (department.children) {
-        sortIndex = getSortIndex(department.children, pId);
-        return !!sortIndex;
-      }
-      return false;
-    });
-  }
-  return sortIndex;
-};
-
 export default observer(() => {
-  const actionRef = useRef<ActionType>();
-  const permissionCode = 'system/Department';
-  const intl = useIntl();
-  const [param, setParam] = useState({});
-  const [expandedRowKeys, setExpandedRowKeys] = useState<React.Key[]>([]);
-  const [treeData, setTreeData] = useState<any[]>([]);
-  const [sortParam, setSortParam] = useState<any>({ name: 'sortIndex', order: 'asc' });
-  const rowKeys = useRef<React.Key[]>([]);
-  const { permission } = PermissionButton.usePermission(permissionCode);
-
-  /**
-   * 根据部门ID删除数据
-   * @param id
-   */
-  const deleteItem = async (id: string) => {
-    const response: any = await service.remove(id);
-    if (response.status === 200) {
-      message.success(
-        intl.formatMessage({
-          id: 'pages.data.option.success',
-          defaultMessage: '操作成功!',
-        }),
-      );
-    }
-    actionRef.current?.reload();
-  };
-
-  const columns: ProColumns<DepartmentItem>[] = [
-    {
-      title: intl.formatMessage({
-        id: 'pages.table.name',
-        defaultMessage: '名称',
-      }),
-      dataIndex: 'name',
-      ellipsis: true,
-    },
-    {
-      title: intl.formatMessage({
-        id: 'pages.device.instanceDetail.detail.sort',
-        defaultMessage: '排序',
-      }),
-      search: false,
-      valueType: 'digit',
-      dataIndex: 'sortIndex',
-      sorter: true,
-    },
-    {
-      title: intl.formatMessage({
-        id: 'pages.data.option',
-        defaultMessage: '操作',
-      }),
-      valueType: 'option',
-      width: 240,
-      render: (text, record) => [
-        <PermissionButton
-          key="editable"
-          tooltip={{
-            title: intl.formatMessage({
-              id: 'pages.data.option.edit',
-              defaultMessage: '编辑',
-            }),
-          }}
-          isPermission={permission.update}
-          style={{ padding: 0 }}
-          type="link"
-          onClick={() => {
-            State.current = record;
-            State.visible = true;
-          }}
-        >
-          <EditOutlined />
-        </PermissionButton>,
-        <PermissionButton
-          key={'addChildren'}
-          style={{ padding: 0 }}
-          tooltip={{
-            title: intl.formatMessage({
-              id: 'pages.system.department.option.add',
-              defaultMessage: '新增子部门',
-            }),
-          }}
-          type="link"
-          isPermission={permission.add}
-          onClick={() => {
-            State.current = {
-              parentId: record.id,
-            };
-            State.visible = true;
-          }}
-        >
-          <PlusCircleOutlined />
-        </PermissionButton>,
-        <PermissionButton
-          key={'assets'}
-          style={{ padding: 0 }}
-          tooltip={{
-            title: intl.formatMessage({
-              id: 'pages.data.option.assets',
-              defaultMessage: '资产分配',
-            }),
-          }}
-          type="link"
-          isPermission={permission.assert}
-          onClick={() => {
-            history.push(
-              `${getMenuPathByParams(
-                MENUS_CODE['system/Department/Detail'],
-                record.id,
-              )}?type=assets`,
-            );
-          }}
-        >
-          <MedicineBoxOutlined />
-        </PermissionButton>,
-        <PermissionButton
-          type="link"
-          key="user"
-          style={{ padding: 0 }}
-          tooltip={{
-            title: intl.formatMessage({
-              id: 'pages.system.department.user',
-              defaultMessage: '用户',
-            }),
-          }}
-          isPermission={permission['bind-user']}
-          onClick={() =>
-            history.push(
-              `${getMenuPathByParams(MENUS_CODE['system/Department/Detail'], record.id)}?type=user`,
-            )
-          }
-        >
-          <TeamOutlined />
-        </PermissionButton>,
-        <PermissionButton
-          type="link"
-          key="delete"
-          style={{ padding: 0 }}
-          popConfirm={{
-            title: intl.formatMessage({
-              id: 'pages.system.role.option.delete',
-              defaultMessage: '确定要删除吗',
-            }),
-            onConfirm() {
-              deleteItem(record.id);
-            },
-          }}
-          tooltip={{
-            title: intl.formatMessage({
-              id: 'pages.data.option.delete',
-              defaultMessage: '删除',
-            }),
-          }}
-          isPermission={permission.delete}
-        >
-          <DeleteOutlined />
-        </PermissionButton>,
-      ],
-    },
-  ];
-
-  const schema: ISchema = {
-    type: 'object',
-    properties: {
-      parentId: {
-        type: 'string',
-        title: '上级部门',
-        'x-decorator': 'FormItem',
-        'x-component': 'TreeSelect',
-        'x-component-props': {
-          fieldNames: {
-            label: 'name',
-            value: 'id',
-          },
-          placeholder: '请选择上级部门',
-        },
-        enum: treeData,
-      },
-      name: {
-        type: 'string',
-        title: intl.formatMessage({
-          id: 'pages.table.name',
-          defaultMessage: '名称',
-        }),
-        required: true,
-        'x-decorator': 'FormItem',
-        'x-component': 'Input',
-        'x-component-props': {
-          placeholder: '请输入名称',
-        },
-        'x-validator': [
-          {
-            max: 64,
-            message: '最多可输入64个字符',
-          },
-          {
-            required: true,
-            message: '请输入名称',
-          },
-        ],
-      },
-      sortIndex: {
-        type: 'string',
-        title: intl.formatMessage({
-          id: 'pages.device.instanceDetail.detail.sort',
-          defaultMessage: '排序',
-        }),
-        required: true,
-        'x-decorator': 'FormItem',
-        'x-component': 'NumberPicker',
-        'x-component-props': {
-          placeholder: '请输入排序',
-        },
-        'x-validator': [
-          {
-            required: true,
-            message: '请输入排序',
-          },
-          {
-            pattern: /^[0-9]*[1-9][0-9]*$/,
-            message: '请输入大于0的整数',
-          },
-        ],
-      },
-    },
-  };
-
-  const location = useLocation();
+  const [parentId, setParentId] = useState('');
+  const [minHeight, setMinHeight] = useState(100);
 
   useEffect(() => {
-    if ((location as any).query?.save === 'true') {
-      State.visible = true;
-    }
+    setTimeout(() => {
+      setMinHeight(getDomFullHeight('department'));
+    }, 0);
+
     /* eslint-disable */
   }, []);
 
   return (
     <PageContainer>
-      <SearchComponent<DepartmentItem>
-        field={columns}
-        defaultParam={[{ column: 'typeId', value: 'org', termType: 'eq' }]}
-        onSearch={async (data) => {
-          // 重置分页数据
-          actionRef.current?.reset?.();
-          setParam(data);
-        }}
-        // onReset={() => {
-        //   // 重置分页及搜索参数
-        //   actionRef.current?.reset?.();
-        //   setParam({});
-        // }}
-        target="department"
-      />
-      <ProTable<DepartmentItem>
-        columns={columns}
-        actionRef={actionRef}
-        request={async (params) => {
-          const response = await service.queryOrgThree({
-            paging: false,
-            sorts: [sortParam],
-            ...params,
-          });
-          setTreeData(response.result);
-          return {
-            code: response.message,
-            result: {
-              data: response.result,
-              pageIndex: 0,
-              pageSize: 0,
-              total: 0,
-            },
-            status: response.status,
-          };
-        }}
-        onChange={(_, f, sorter: any) => {
-          if (sorter.order) {
-            setSortParam({ name: sorter.columnKey, order: sorter.order.replace('end', '') });
-          } else {
-            setSortParam({ name: 'sortIndex', value: 'asc' });
-          }
-        }}
-        rowKey="id"
-        expandable={{
-          expandedRowKeys: [...rowKeys.current],
-          onExpandedRowsChange: (keys) => {
-            rowKeys.current = keys as React.Key[];
-            setExpandedRowKeys(keys as React.Key[]);
-          },
-        }}
-        pagination={false}
-        search={false}
-        params={param}
-        headerTitle={
-          <PermissionButton
-            isPermission={permission.add}
-            onClick={() => {
-              State.visible = true;
-            }}
-            key="button"
-            icon={<PlusOutlined />}
-            type="primary"
-          >
-            {intl.formatMessage({
-              id: 'pages.data.option.add',
-              defaultMessage: '新增',
-            })}
-          </PermissionButton>
-        }
-      />
-      <Save<DepartmentItem>
-        parentChange={(pId) => {
-          return getSortIndex(treeData, pId);
-        }}
-        title={
-          State.current.parentId
-            ? intl.formatMessage({
-                id: 'pages.system.department.option.add',
-                defaultMessage: '新增子部门',
-              })
-            : undefined
-        }
-        service={service}
-        onCancel={(type, pId) => {
-          if (pId) {
-            expandedRowKeys.push(pId);
-            rowKeys.current.push(pId);
-            setExpandedRowKeys(expandedRowKeys);
-          }
-          if (type) {
-            actionRef.current?.reload();
-          }
-          State.current = {};
-          State.visible = false;
-        }}
-        data={State.current}
-        visible={State.visible}
-        schema={schema}
-      />
+      <Card className={'department'} style={{ minHeight }}>
+        <div className={'department-warp'}>
+          <div className={'department-left'}>
+            <Tree onSelect={setParentId} />
+          </div>
+          <div className={'department-right'}>
+            <Assets parentId={parentId} />
+          </div>
+        </div>
+      </Card>
+      {/*<SearchComponent<DepartmentItem>*/}
+      {/*  field={columns}*/}
+      {/*  defaultParam={[{ column: 'typeId', value: 'org', termType: 'eq' }]}*/}
+      {/*  onSearch={async (data) => {*/}
+      {/*    // 重置分页数据*/}
+      {/*    actionRef.current?.reset?.();*/}
+      {/*    setParam(data);*/}
+      {/*  }}*/}
+      {/*  // onReset={() => {*/}
+      {/*  //   // 重置分页及搜索参数*/}
+      {/*  //   actionRef.current?.reset?.();*/}
+      {/*  //   setParam({});*/}
+      {/*  // }}*/}
+      {/*  target="department"*/}
+      {/*/>*/}
+      {/*<ProTable<DepartmentItem>*/}
+      {/*  columns={columns}*/}
+      {/*  actionRef={actionRef}*/}
+      {/*  request={async (params) => {*/}
+      {/*    const response = await service.queryOrgThree({*/}
+      {/*      paging: false,*/}
+      {/*      sorts: [sortParam],*/}
+      {/*      ...params,*/}
+      {/*    });*/}
+      {/*    setTreeData(response.result);*/}
+      {/*    return {*/}
+      {/*      code: response.message,*/}
+      {/*      result: {*/}
+      {/*        data: response.result,*/}
+      {/*        pageIndex: 0,*/}
+      {/*        pageSize: 0,*/}
+      {/*        total: 0,*/}
+      {/*      },*/}
+      {/*      status: response.status,*/}
+      {/*    };*/}
+      {/*  }}*/}
+      {/*  onChange={(_, f, sorter: any) => {*/}
+      {/*    if (sorter.order) {*/}
+      {/*      setSortParam({ name: sorter.columnKey, order: sorter.order.replace('end', '') });*/}
+      {/*    } else {*/}
+      {/*      setSortParam({ name: 'sortIndex', value: 'asc' });*/}
+      {/*    }*/}
+      {/*  }}*/}
+      {/*  rowKey="id"*/}
+      {/*  expandable={{*/}
+      {/*    expandedRowKeys: [...rowKeys.current],*/}
+      {/*    onExpandedRowsChange: (keys) => {*/}
+      {/*      rowKeys.current = keys as React.Key[];*/}
+      {/*      setExpandedRowKeys(keys as React.Key[]);*/}
+      {/*    },*/}
+      {/*  }}*/}
+      {/*  pagination={false}*/}
+      {/*  search={false}*/}
+      {/*  params={param}*/}
+      {/*  headerTitle={*/}
+      {/*    <PermissionButton*/}
+      {/*      isPermission={permission.add}*/}
+      {/*      onClick={() => {*/}
+      {/*        State.visible = true;*/}
+      {/*      }}*/}
+      {/*      key="button"*/}
+      {/*      icon={<PlusOutlined />}*/}
+      {/*      type="primary"*/}
+      {/*    >*/}
+      {/*      {intl.formatMessage({*/}
+      {/*        id: 'pages.data.option.add',*/}
+      {/*        defaultMessage: '新增',*/}
+      {/*      })}*/}
+      {/*    </PermissionButton>*/}
+      {/*  }*/}
+      {/*/>*/}
+      {/*<Save<DepartmentItem>*/}
+      {/*  parentChange={(pId) => {*/}
+      {/*    return getSortIndex(treeData, pId);*/}
+      {/*  }}*/}
+      {/*  title={*/}
+      {/*    State.current.parentId*/}
+      {/*      ? intl.formatMessage({*/}
+      {/*          id: 'pages.system.department.option.add',*/}
+      {/*          defaultMessage: '新增子部门',*/}
+      {/*        })*/}
+      {/*      : undefined*/}
+      {/*  }*/}
+      {/*  service={service}*/}
+      {/*  onCancel={(type, pId) => {*/}
+      {/*    if (pId) {*/}
+      {/*      expandedRowKeys.push(pId);*/}
+      {/*      rowKeys.current.push(pId);*/}
+      {/*      setExpandedRowKeys(expandedRowKeys);*/}
+      {/*    }*/}
+      {/*    if (type) {*/}
+      {/*      actionRef.current?.reload();*/}
+      {/*    }*/}
+      {/*    State.current = {};*/}
+      {/*    State.visible = false;*/}
+      {/*  }}*/}
+      {/*  data={State.current}*/}
+      {/*  visible={State.visible}*/}
+      {/*  schema={schema}*/}
+      {/*/>*/}
     </PageContainer>
   );
 });

+ 5 - 13
src/pages/system/Department/save.tsx

@@ -1,7 +1,6 @@
 // Modal 弹窗,用于新增、修改数据
 import React from 'react';
-import type { Field } from '@formily/core';
-import { createForm, onFieldReact } from '@formily/core';
+import { createForm } from '@formily/core';
 import { createSchemaField } from '@formily/react';
 import {
   ArrayTable,
@@ -30,14 +29,13 @@ import type BaseService from '@/utils/BaseService';
 export interface SaveModalProps<T> extends Omit<ModalProps, 'onOk' | 'onCancel'> {
   service: BaseService<T>;
   data?: Partial<T>;
-  reload?: () => void;
+  reload?: (pId: string) => void;
   /**
    * Model关闭事件
    * @param type 是否为请求接口后关闭,用于外部table刷新数据
    */
   onCancel?: (type: boolean, id?: React.Key) => void;
   schema: ISchema;
-  parentChange: (value?: string) => number;
 }
 
 const Save = <T extends object>(props: SaveModalProps<T>) => {
@@ -71,15 +69,6 @@ const Save = <T extends object>(props: SaveModalProps<T>) => {
   const form = createForm({
     validateFirst: true,
     initialValues: data || {},
-    effects: () => {
-      onFieldReact('sortIndex', (field) => {
-        const value = (field as Field).value;
-        if (props.parentChange && !value) {
-          const sortIndex = props.parentChange(field.query('parentId').value());
-          (field as Field).value = !!sortIndex ? sortIndex : sortIndex + 1;
-        }
-      });
-    },
   });
 
   /**
@@ -105,6 +94,9 @@ const Save = <T extends object>(props: SaveModalProps<T>) => {
     if (response.status === 200) {
       message.success('操作成功!');
       modalClose(true, response.result.parentId);
+      if (props.reload) {
+        props.reload(response.result.parentId);
+      }
       if ((window as any).onTabSaveSuccess) {
         (window as any).onTabSaveSuccess(response.result);
         setTimeout(() => window.close(), 300);

+ 1 - 0
src/pages/system/Department/style.ts

@@ -0,0 +1 @@
+import './index.less';

+ 3 - 1
src/pages/system/Department/typings.d.ts

@@ -27,6 +27,7 @@ export type ProductItem = {
   id: string;
   name: string;
   description: string;
+  grantedPermissions?: string[];
 };
 
 // 产品分类
@@ -37,6 +38,7 @@ export type DeviceItem = {
   id: string;
   name: string;
   productName: string;
-  createTime: string;
+  createTime: number;
   state: State;
+  grantedPermissions?: string[];
 };

+ 18 - 0
src/utils/util.ts

@@ -100,3 +100,21 @@ export const randomString = (length?: number) => {
   }
   return pwd;
 };
+
+/**
+ * 获取当前DOM元素需要撑满的高度
+ * @param className
+ * @param extraHeight 额外减去的高度
+ */
+export const getDomFullHeight = (className: string, extraHeight: number = 0): number => {
+  const dom = document.querySelector(`.${className}`);
+  if (dom) {
+    const bodyClient = document.body.getBoundingClientRect();
+    const domClient = dom.getBoundingClientRect();
+    if (domClient.y < 50) {
+      return 100;
+    }
+    return bodyClient.height - domClient.y - 24 - extraHeight;
+  }
+  return 0;
+};