Parcourir la source

feat(tenant): Tenant Manager

Lind il y a 4 ans
Parent
commit
44cee92f19

+ 3 - 4
src/components/Authorization/index.less

@@ -1,11 +1,10 @@
 .action {
-  position: absolute;
+  position: fixed;
   right: 0;
   bottom: 0;
-  width: 100%;
-  padding: 10px 16px;
+  width: 50%;
+  padding: 16px;
   text-align: right;
-  background: 10px 16px;
   border-top: 1px solid #e9e9e9;
 }
 

+ 24 - 22
src/components/Authorization/index.tsx

@@ -54,6 +54,19 @@ const Authorization = observer((props: AuthorizationProps) => {
     [AuthorizationModel.data],
   );
 
+  const searchPermission = async (name: string, type: string) => {
+    AuthorizationModel.filterParam.name = name;
+    AuthorizationModel.filterParam.type = type;
+    AuthorizationModel.data = await db
+      .table(tableName)
+      .where('name')
+      .startsWith(name)
+      .filter((item) => (type === 'all' ? item : (item.type || []).includes(type)))
+      .distinct()
+      .reverse()
+      .sortBy('name');
+  };
+
   const initAutzInfo = useCallback(async () => {
     if (!target.id) {
       message.error('被授权对象数据缺失!');
@@ -92,6 +105,10 @@ const Authorization = observer((props: AuthorizationProps) => {
         error: () => {},
         complete: () => {
           AuthorizationModel.spinning = false;
+
+          if (props.type) {
+            searchPermission('', props.type);
+          }
         },
       });
   }, [target.id]);
@@ -115,25 +132,13 @@ const Authorization = observer((props: AuthorizationProps) => {
 
   useEffect(() => {
     initPermission();
+
     return () => {
       db.table(tableName).clear();
       AuthorizationModel.spinning = true;
     };
   }, [target.id]);
 
-  const searchPermission = async (name: string, type: string) => {
-    AuthorizationModel.filterParam.name = name;
-    AuthorizationModel.filterParam.type = type;
-    AuthorizationModel.data = await db
-      .table(tableName)
-      .where('name')
-      .startsWith(name)
-      .filter((item) => (type === 'all' ? item : (item.type || []).includes(type)))
-      .distinct()
-      .reverse()
-      .sortBy('name');
-  };
-
   const setAutz = (data: unknown[]) => {
     const permissions = Object.keys(data)
       .filter((i) => !i.startsWith('_'))
@@ -244,12 +249,7 @@ const Authorization = observer((props: AuthorizationProps) => {
         wrapperCol={{ span: 20 }}
         labelCol={{ span: 3 }}
       >
-        <Form.Item
-          label={intl.formatMessage({
-            id: 'pages.analysis.cpu',
-            defaultMessage: '被授权主体',
-          })}
-        >
+        <Form.Item label="被授权主体">
           <Input value={target.name} disabled={true} />
         </Form.Item>
         <Form.Item
@@ -263,10 +263,12 @@ const Authorization = observer((props: AuthorizationProps) => {
               <Col span={4}>
                 <Select
                   onSelect={(type: string) =>
-                    searchPermission(AuthorizationModel.filterParam.name, type)
+                    searchPermission(AuthorizationModel.filterParam.name, props.type || type)
                   }
                   style={{ width: '100%' }}
-                  defaultValue={'all'}
+                  defaultValue={props.type || 'all'}
+                  // 如果传了类型,那么授权不能更改类型
+                  disabled={!!props.type}
                   options={permissionType}
                 />
               </Col>
@@ -277,7 +279,7 @@ const Authorization = observer((props: AuthorizationProps) => {
                     defaultMessage: '请输入权限名称',
                   })}
                   onSearch={(name: string) =>
-                    searchPermission(name, AuthorizationModel.filterParam?.type)
+                    searchPermission(name, props.type || AuthorizationModel.filterParam?.type)
                   }
                 />
               </Col>

+ 1 - 0
src/components/Authorization/typings.d.ts

@@ -37,4 +37,5 @@ interface AuthorizationProps {
     type: string;
   };
   close: () => void;
+  type?: string;
 }

+ 74 - 7
src/pages/system/Tenant/Detail/Assets/index.tsx

@@ -2,12 +2,79 @@ import ProCard from '@ant-design/pro-card';
 import { EditOutlined, EyeOutlined } from '@ant-design/icons';
 import { Card, Form, Row, Select, Statistic } from 'antd';
 import { Col } from 'antd';
+import { useEffect } from 'react';
+import { observer } from '@formily/react';
+import { service } from '@/pages/system/Tenant';
+import { useParams } from 'umi';
+import TenantModel from '@/pages/system/Tenant/model';
+import type { TenantMember } from '@/pages/system/Tenant/typings';
+import encodeQuery from '@/utils/encodeQuery';
 
-const Assets = () => {
+const Assets = observer(() => {
+  const param = useParams<{ id: string }>();
+
+  const getDeviceCount = (type: 'online' | 'offline') => {
+    // 查询资产数量
+    service.assets
+      .deviceCount(
+        encodeQuery({
+          terms: {
+            state: type,
+            id$assets: {
+              tenantId: param.id,
+              assetType: 'device',
+              memberId: TenantModel.assetsMemberId,
+            },
+          },
+        }),
+      )
+      .subscribe((data) => {
+        TenantModel.assets.device[type] = data;
+      });
+  };
+
+  // 1\0 已发布\未发布
+  const getProductCount = (type: 1 | 0) => {
+    service.assets
+      .productCount(
+        encodeQuery({
+          terms: {
+            state: type,
+            id$assets: {
+              tenantId: param.id,
+              assetType: 'product',
+              memberId: TenantModel.assetsMemberId,
+            },
+          },
+        }),
+      )
+      .subscribe((data) => {
+        TenantModel.assets.product[type] = data;
+      });
+  };
+
+  useEffect(() => {
+    // 查询成员
+    service.queryMemberNoPaging(param.id).subscribe((data: TenantMember[]) => {
+      TenantModel.members = data;
+    });
+  }, []);
+
+  useEffect(() => {
+    getProductCount(1);
+    getProductCount(0);
+    getDeviceCount('online');
+    getDeviceCount('offline');
+  }, [TenantModel.assetsMemberId]);
   return (
     <Card>
       <Form.Item label="成员" style={{ width: 200 }}>
-        <Select />
+        <Select
+          onChange={(id: string) => {
+            TenantModel.assetsMemberId = id;
+          }}
+          options={TenantModel.members.map((item) => ({ label: item.name, value: item.userId }))}
+        />
       </Form.Item>
       <ProCard gutter={[16, 16]} style={{ marginTop: 16 }}>
         <ProCard
@@ -18,10 +85,10 @@ const Assets = () => {
         >
           <Row>
             <Col span={12}>
-              <Statistic title="已发布" value={20} />
+              <Statistic title="已发布" value={TenantModel.assets.product['1']} />
             </Col>
             <Col span={12}>
-              <Statistic title="未发布" value={19} />
+              <Statistic title="未发布" value={TenantModel.assets.product['0']} />
             </Col>
           </Row>
         </ProCard>
@@ -34,15 +101,15 @@ const Assets = () => {
         >
           <Row>
             <Col span={12}>
-              <Statistic title="已发布" value={20} />
+              <Statistic title="在线" value={TenantModel.assets.device.online} />
             </Col>
             <Col span={12}>
-              <Statistic title="未发布" value={19} />
+              <Statistic title="离线" value={TenantModel.assets.device.offline} />
             </Col>
           </Row>
         </ProCard>
       </ProCard>
     </Card>
   );
-};
+});
 export default Assets;

+ 85 - 0
src/pages/system/Tenant/Detail/Member/Bind.tsx

@@ -0,0 +1,85 @@
+import type { ProColumns, ActionType } from '@jetlinks/pro-table';
+import ProTable from '@jetlinks/pro-table';
+import { service } from '@/pages/system/Tenant';
+import { message, Space } from 'antd';
+import { useParams } from 'umi';
+import TenantModel from '@/pages/system/Tenant/model';
+import { observer } from '@formily/react';
+import { useRef } from 'react';
+
+interface Props {
+  reload: () => void;
+}
+
+const Bind = observer((props: Props) => {
+  const param = useParams<{ id: string }>();
+  const actionRef = useRef<ActionType>();
+  const columns: ProColumns<UserItem>[] = [
+    {
+      dataIndex: 'name',
+      title: '姓名',
+      search: {
+        transform: (value) => ({ name$LIKE: value }),
+      },
+    },
+    {
+      dataIndex: 'username',
+      title: '用户名',
+      search: {
+        transform: (value) => ({ username$LIKE: value }),
+      },
+    },
+  ];
+
+  const handleBind = () => {
+    service.handleUser(param.id, TenantModel.bindUsers, 'bind').subscribe({
+      next: () => message.success('操作成功'),
+      error: () => message.error('操作失败'),
+      complete: () => {
+        TenantModel.bindUsers = [];
+        actionRef.current?.reload();
+        props.reload();
+      },
+    });
+  };
+
+  return (
+    <ProTable
+      actionRef={actionRef}
+      columns={columns}
+      rowKey="id"
+      pagination={{
+        pageSize: 5,
+      }}
+      tableAlertRender={({ selectedRowKeys, onCleanSelected }) => (
+        <Space size={24}>
+          <span>
+            已选 {selectedRowKeys.length} 项
+            <a style={{ marginLeft: 8 }} onClick={onCleanSelected}>
+              取消选择
+            </a>
+          </span>
+        </Space>
+      )}
+      tableAlertOptionRender={() => (
+        <Space size={16}>
+          <a onClick={handleBind}>批量绑定</a>
+        </Space>
+      )}
+      rowSelection={{
+        selectedRowKeys: TenantModel.bindUsers.map((item) => item.userId),
+        onChange: (selectedRowKeys, selectedRows) => {
+          TenantModel.bindUsers = selectedRows.map((item) => ({
+            name: item.name,
+            userId: item.id,
+          }));
+        },
+      }}
+      request={(params) => service.queryUser(params)}
+      defaultParams={{
+        'id$tenant-user$not': param.id,
+      }}
+    />
+  );
+});
+export default Bind;

+ 87 - 36
src/pages/system/Tenant/Detail/Member/index.tsx

@@ -1,12 +1,18 @@
-import type { ProColumns } from '@jetlinks/pro-table';
+import type { ProColumns, ActionType } from '@jetlinks/pro-table';
 import ProTable from '@jetlinks/pro-table';
-import { Tooltip } from 'antd';
-import { EyeOutlined, UnlockFilled } from '@ant-design/icons';
+import { Button, Card, Col, message, Row, Space } from 'antd';
+import { CloseOutlined, PlusOutlined } from '@ant-design/icons';
 import type { TenantMember } from '@/pages/system/Tenant/typings';
 import { service } from '@/pages/system/Tenant';
 import { useParams } from 'umi';
+import Bind from '@/pages/system/Tenant/Detail/Member/Bind';
+import { observer } from '@formily/react';
+import TenantModel from '@/pages/system/Tenant/model';
+import { useRef } from 'react';
+
+const Member = observer(() => {
+  const actionRef = useRef<ActionType>();
 
-const Member = () => {
   const param = useParams<{ id: string }>();
   const columns: ProColumns<TenantMember>[] = [
     {
@@ -33,39 +39,84 @@ const Member = () => {
       renderText: (text) => text.text,
       search: false,
     },
-    {
-      title: '操作',
-      valueType: 'option',
-      render: (text, record) => [
-        <a
-          key="edit"
-          onClick={() => {
-            console.log(JSON.stringify(record));
-          }}
-        >
-          <Tooltip title="查看资产">
-            <EyeOutlined />
-          </Tooltip>
-        </a>,
-        <a
-          key="bind"
-          onClick={() => {
-            console.log(JSON.stringify(record));
-          }}
-        >
-          <Tooltip title="解绑">
-            <UnlockFilled />
-          </Tooltip>
-        </a>,
-      ],
-    },
   ];
+  const handleUnBind = () => {
+    service.handleUser(param.id, TenantModel.unBindUsers, 'unbind').subscribe({
+      next: () => message.success('操作成功'),
+      error: () => message.error('操作失败'),
+      complete: () => {
+        TenantModel.unBindUsers = [];
+        actionRef.current?.reload();
+      },
+    });
+  };
   return (
-    <ProTable
-      columns={columns}
-      rowKey="id"
-      request={(params) => service.queryMembers(param.id, params)}
-    />
+    <Row gutter={[16, 16]}>
+      <Col span={TenantModel.bind ? 12 : 24}>
+        <Card title="租户成员">
+          <ProTable
+            actionRef={actionRef}
+            columns={columns}
+            rowKey="id"
+            pagination={{
+              pageSize: 5,
+            }}
+            tableAlertRender={({ selectedRowKeys, onCleanSelected }) => (
+              <Space size={24}>
+                <span>
+                  已选 {selectedRowKeys.length} 项
+                  <a style={{ marginLeft: 8 }} onClick={onCleanSelected}>
+                    取消选择
+                  </a>
+                </span>
+              </Space>
+            )}
+            tableAlertOptionRender={() => (
+              <Space size={16}>
+                <a onClick={handleUnBind}>批量解绑</a>
+              </Space>
+            )}
+            rowSelection={{
+              selectedRowKeys: TenantModel.unBindUsers,
+              onChange: (selectedRowKeys, selectedRows) => {
+                TenantModel.unBindUsers = selectedRows.map((item) => item.id);
+              },
+            }}
+            request={(params) => service.queryMembers(param.id, params)}
+            toolBarRender={() => [
+              <Button
+                size="small"
+                onClick={() => {
+                  TenantModel.bind = true;
+                }}
+                icon={<PlusOutlined />}
+                type="primary"
+                key="bind"
+              >
+                绑定用户
+              </Button>,
+            ]}
+          />
+        </Card>
+      </Col>
+      {TenantModel.bind && (
+        <Col span={12}>
+          <Card
+            title="添加用户"
+            extra={
+              <CloseOutlined
+                onClick={() => {
+                  TenantModel.bind = false;
+                  TenantModel.bindUsers = [];
+                }}
+              />
+            }
+          >
+            <Bind reload={() => actionRef.current?.reload()} />
+          </Card>
+        </Col>
+      )}
+    </Row>
   );
-};
+});
 export default Member;

+ 0 - 11
src/pages/system/Tenant/Detail/index.tsx

@@ -7,7 +7,6 @@ import { service } from '@/pages/system/Tenant';
 import Assets from '@/pages/system/Tenant/Detail/Assets';
 import Member from '@/pages/system/Tenant/Detail/Member';
 import Info from '@/pages/system/Tenant/Detail/Info';
-import Permission from './Permission';
 
 const TenantDetail = observer(() => {
   const [tab, setTab] = useState<string>('assets');
@@ -28,11 +27,6 @@ const TenantDetail = observer(() => {
   }, [params.id]);
 
   const list = [
-    // {
-    //   key: 'detail',
-    //   tab: '基本信息',
-    //   component: <Info/>
-    // },
     {
       key: 'assets',
       tab: '资产信息',
@@ -43,11 +37,6 @@ const TenantDetail = observer(() => {
       tab: '成员管理',
       component: <Member />,
     },
-    {
-      key: 'permission',
-      tab: '权限管理',
-      component: <Permission />,
-    },
   ];
 
   return (

+ 34 - 42
src/pages/system/Tenant/index.tsx

@@ -4,23 +4,21 @@ import type { TenantDetail } from '@/pages/system/Tenant/typings';
 import type { TenantItem } from '@/pages/system/Tenant/typings';
 import BaseCrud from '@/components/BaseCrud';
 import { useRef } from 'react';
-import { Avatar, message, Popconfirm, Tooltip } from 'antd';
+import { Avatar, Drawer, Tooltip } from 'antd';
 import Service from '@/pages/system/Tenant/service';
-import {
-  CloseCircleOutlined,
-  EyeOutlined,
-  KeyOutlined,
-  PlayCircleOutlined,
-} from '@ant-design/icons';
+import { EyeOutlined, KeyOutlined } from '@ant-design/icons';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import moment from 'moment';
 import { Link } from 'umi';
 import TenantModel from '@/pages/system/Tenant/model';
 import type { ISchema } from '@formily/json-schema';
+import autzModel from '@/components/Authorization/autz';
+import Authorization from '@/components/Authorization';
+import { observer } from '@formily/react';
 
 export const service = new Service('tenant');
 
-const Tenant = () => {
+const Tenant = observer(() => {
   const intl = useIntl();
   const actionRef = useRef<ActionType>();
   const columns: ProColumns<TenantItem>[] = [
@@ -133,7 +131,14 @@ const Tenant = () => {
           </Tooltip>
         </Link>,
 
-        <a onClick={() => console.log('授权')}>
+        <a
+          key="auth"
+          onClick={() => {
+            autzModel.autzTarget.id = record.tenant.id!;
+            autzModel.autzTarget.name = record.tenant.name!;
+            autzModel.visible = true;
+          }}
+        >
           <Tooltip
             title={intl.formatMessage({
               id: 'pages.data.option.authorize',
@@ -143,38 +148,6 @@ const Tenant = () => {
             <KeyOutlined />
           </Tooltip>
         </a>,
-        <a href={record.tenant.id} target="_blank" rel="noopener noreferrer" key="view">
-          <Popconfirm
-            title={intl.formatMessage({
-              id: 'pages.data.option.disabled.tips',
-              defaultMessage: '确认禁用?',
-            })}
-            onConfirm={async () => {
-              await service.update({
-                tenant: {
-                  id: record.tenant.id,
-                  state: record.tenant?.state.value ? 0 : 1,
-                },
-              });
-              message.success(
-                intl.formatMessage({
-                  id: 'pages.data.option.success',
-                  defaultMessage: '操作成功!',
-                }),
-              );
-              actionRef.current?.reload();
-            }}
-          >
-            <Tooltip
-              title={intl.formatMessage({
-                id: `pages.data.option.${record.tenant?.state.value ? 'disabled' : 'enabled'}`,
-                defaultMessage: record.tenant?.state?.value ? '禁用' : '启用',
-              })}
-            >
-              {record.tenant?.state.value ? <CloseCircleOutlined /> : <PlayCircleOutlined />}
-            </Tooltip>
-          </Popconfirm>
-        </a>,
       ],
     },
   ];
@@ -261,7 +234,26 @@ const Tenant = () => {
         schema={schema}
         actionRef={actionRef}
       />
+      <Drawer
+        title={intl.formatMessage({
+          id: 'pages.data.option.authorize',
+          defaultMessage: '授权',
+        })}
+        width="50vw"
+        visible={autzModel.visible}
+        onClose={() => {
+          autzModel.visible = false;
+        }}
+      >
+        <Authorization
+          close={() => {
+            autzModel.visible = false;
+          }}
+          target={autzModel.autzTarget}
+          type={'tenant'}
+        />
+      </Drawer>
     </PageContainer>
   );
-};
+});
 export default Tenant;

+ 31 - 0
src/pages/system/Tenant/model.ts

@@ -1,13 +1,44 @@
 import { model } from '@formily/reactive';
 import type { TenantDetail } from '@/pages/system/Tenant/typings';
+import type { TenantMember } from '@/pages/system/Tenant/typings';
 
 type TenantModelType = {
   current: Partial<TenantDetail>;
   detail: Partial<TenantDetail>;
+  bind: boolean;
+  bindUsers: { name: string; userId: string }[];
+  unBindUsers: string[];
+  members: TenantMember[];
+  assets: {
+    device: {
+      online: number;
+      offline: number;
+    };
+    product: {
+      0: number;
+      1: number;
+    };
+  };
+  assetsMemberId: string | undefined;
 };
 const TenantModel = model<TenantModelType>({
   current: {},
   detail: {},
+  bind: false,
+  bindUsers: [],
+  unBindUsers: [],
+  members: [],
+  assets: {
+    device: {
+      online: 0,
+      offline: 0,
+    },
+    product: {
+      0: 0,
+      1: 0,
+    },
+  },
+  assetsMemberId: undefined,
 });
 
 export default TenantModel;

+ 55 - 0
src/pages/system/Tenant/service.ts

@@ -3,6 +3,7 @@ import BaseService from '@/utils/BaseService';
 import { request } from 'umi';
 import { defer, from } from 'rxjs';
 import { filter, map } from 'rxjs/operators';
+import SystemConst from '@/utils/const';
 
 class Service extends BaseService<TenantItem> {
   queryList = (params: any) =>
@@ -27,6 +28,60 @@ class Service extends BaseService<TenantItem> {
 
   queryMembers = (id: string, params: Record<string, unknown>) =>
     request(`${this.uri}/${id}/members/_query`, { method: 'GET', params });
+
+  queryMemberNoPaging = (id: string) =>
+    defer(() =>
+      from(request(`${this.uri}/${id}/members/_query/no-paging?paging=false`, { method: 'GET' })),
+    ).pipe(
+      filter((item) => item.status === 200),
+      map((item) => item.result),
+    );
+
+  queryUser = (params: Record<string, unknown>) =>
+    request(`/${SystemConst.API_BASE}/user/_query`, {
+      method: 'GET',
+      params,
+    });
+
+  handleUser = (id: string, data: Record<string, unknown>[] | string[], type: 'bind' | 'unbind') =>
+    defer(() =>
+      from(
+        request(`/${SystemConst.API_BASE}/tenant/${id}/members/_${type}`, {
+          method: 'POST',
+          data,
+        }),
+      ),
+    ).pipe(
+      filter((item) => item.status === 200),
+      map((item) => item.result),
+    );
+
+  assets = {
+    deviceCount: (params: any) =>
+      defer(() =>
+        from(
+          request(`/${SystemConst.API_BASE}/device/instance/_count`, {
+            method: 'GET',
+            params,
+          }),
+        ).pipe(
+          filter((resp) => resp.status === 200),
+          map((resp) => resp.result),
+        ),
+      ),
+    productCount: (params: any) =>
+      defer(() =>
+        from(
+          request(`/${SystemConst.API_BASE}/device-product/_count`, {
+            method: 'GET',
+            params,
+          }),
+        ).pipe(
+          filter((resp) => resp.status === 200),
+          map((resp) => resp.result),
+        ),
+      ),
+  };
 }
 
 export default Service;