sun-chaochao 3 лет назад
Родитель
Сommit
6ba1cc2904
47 измененных файлов с 2709 добавлено и 743 удалено
  1. 7 0
      config/routes.ts
  2. 1 2
      package.json
  3. 22 3
      src/components/BaseCrud/index.tsx
  4. 1 0
      src/components/BaseCrud/model.ts
  5. 9 1
      src/components/SearchComponent/GroupNameControl.tsx
  6. 92 66
      src/components/SearchComponent/index.tsx
  7. 10 0
      src/components/SearchComponent/typings.d.ts
  8. 1 0
      src/locales/en-US/pages.ts
  9. 3 0
      src/locales/zh-CN/menu.ts
  10. 5 0
      src/locales/zh-CN/pages.ts
  11. 141 0
      src/pages/system/Department/Assets/deivce/bind.tsx
  12. 274 0
      src/pages/system/Department/Assets/deivce/index.tsx
  13. 16 0
      src/pages/system/Department/Assets/deivce/model.ts
  14. 55 0
      src/pages/system/Department/Assets/index.tsx
  15. 108 0
      src/pages/system/Department/Assets/permissionModal.tsx
  16. 123 0
      src/pages/system/Department/Assets/product/bind.tsx
  17. 210 0
      src/pages/system/Department/Assets/product/index.tsx
  18. 16 0
      src/pages/system/Department/Assets/product/model.ts
  19. 131 0
      src/pages/system/Department/Assets/productCategory/bind.tsx
  20. 244 0
      src/pages/system/Department/Assets/productCategory/index.tsx
  21. 16 0
      src/pages/system/Department/Assets/productCategory/model.ts
  22. 48 0
      src/pages/system/Department/Assets/service.ts
  23. 98 0
      src/pages/system/Department/Member/bind.tsx
  24. 214 0
      src/pages/system/Department/Member/index.tsx
  25. 16 0
      src/pages/system/Department/Member/model.ts
  26. 29 0
      src/pages/system/Department/Member/service.ts
  27. 250 0
      src/pages/system/Department/index.tsx
  28. 117 0
      src/pages/system/Department/save.tsx
  29. 11 0
      src/pages/system/Department/service.ts
  30. 42 0
      src/pages/system/Department/typings.d.ts
  31. 5 5
      src/pages/system/OpenAPI/index.tsx
  32. 0 59
      src/pages/system/Org/NodeTemplate/index.tsx
  33. 0 101
      src/pages/system/Org/Save/index.tsx
  34. 0 70
      src/pages/system/Org/index.less
  35. 0 179
      src/pages/system/Org/index.tsx
  36. 0 33
      src/pages/system/Org/model.ts
  37. 0 11
      src/pages/system/Org/service.ts
  38. 0 32
      src/pages/system/Org/typings.d.ts
  39. 2 11
      src/pages/system/Permission/index.tsx
  40. 2 12
      src/pages/system/Role/index.tsx
  41. 5 5
      src/pages/system/Tenant/index.tsx
  42. 258 0
      src/pages/system/User/Save/index.tsx
  43. 52 151
      src/pages/system/User/index.tsx
  44. 41 0
      src/pages/system/User/serivce.ts
  45. 5 0
      src/pages/system/User/typings.d.ts
  46. 2 2
      src/utils/BaseService.ts
  47. 27 0
      src/utils/menu.ts

+ 7 - 0
config/routes.ts

@@ -45,6 +45,13 @@
         component: './system/Role',
       },
       {
+        hideInMenu: true,
+        path: '/system/role/edit/:id',
+        name: 'role-edit',
+        icon: 'smile',
+        component: './system/Role/Edit',
+      },
+      {
         path: '/system/permission',
         name: 'permission',
         icon: 'smile',

+ 1 - 2
package.json

@@ -62,7 +62,6 @@
     "@ant-design/pro-descriptions": "^1.6.8",
     "@ant-design/pro-form": "^1.18.3",
     "@ant-design/pro-layout": "^6.27.2",
-    "@dabeng/react-orgchart": "^1.0.0",
     "@formily/antd": "2.0.0-rc.17",
     "@formily/core": "2.0.0-rc.17",
     "@formily/json-schema": "2.0.0-rc.17",
@@ -71,7 +70,7 @@
     "@formily/reactive-react": "2.0.0-rc.17",
     "@formily/shared": "2.0.0-rc.17",
     "@jetlinks/pro-list": "^1.10.8",
-    "@jetlinks/pro-table": "^2.43.8",
+    "@jetlinks/pro-table": "^2.63.8",
     "@umijs/route-utils": "^1.0.36",
     "ahooks": "^2.10.9",
     "antd": "^4.17.0-alpha.9",

+ 22 - 3
src/components/BaseCrud/index.tsx

@@ -1,5 +1,5 @@
 import { useIntl } from '@@/plugin-locale/localeExports';
-import { Button, Dropdown } from 'antd';
+import { Button, Card, Divider, Dropdown } from 'antd';
 import ProTable from '@jetlinks/pro-table';
 import type { ProColumns, ActionType, RequestData } from '@jetlinks/pro-table';
 
@@ -12,8 +12,11 @@ import { CurdModel } from '@/components/BaseCrud/model';
 import type { ISchemaFieldProps } from '@formily/react/lib/types';
 import type { ModalProps } from 'antd/lib/modal/Modal';
 import type { TablePaginationConfig } from 'antd/lib/table/interface';
-import type { SearchConfig } from '@jetlinks/pro-table/lib/components/Form/FormRender';
 import type { Form } from '@formily/core';
+import SearchComponent from '@/components/SearchComponent';
+import { useRef, useState } from 'react';
+import type { ProFormInstance } from '@ant-design/pro-form';
+import type { SearchConfig } from '@ant-design/pro-form/lib/components/Submitter';
 
 export type Option = {
   model: 'edit' | 'preview' | 'add';
@@ -41,11 +44,13 @@ export type Props<T> = {
   search?: false | SearchConfig;
   formEffect?: () => void; // 与form参数 只有一个生效
   form?: Form;
+  /** @name 用于存储搜索历史记录的标记*/
+  moduleName?: string; //
 };
 
 const BaseCrud = <T extends Record<string, any>>(props: Props<T>) => {
   const intl = useIntl();
-
+  const ref = useRef<ProFormInstance>();
   const {
     columns,
     service,
@@ -62,11 +67,25 @@ const BaseCrud = <T extends Record<string, any>>(props: Props<T>) => {
     search,
     formEffect,
     form,
+    moduleName,
   } = props;
 
+  const [param, setParam] = useState({});
   return (
     <>
+      <Card>
+        <SearchComponent<T>
+          field={columns}
+          onSearch={async (data) => {
+            setParam({ terms: data });
+          }}
+          target={moduleName}
+        />
+      </Card>
+      <Divider />
       <ProTable<T>
+        params={param}
+        formRef={ref}
         columns={columns}
         actionRef={actionRef}
         options={{ fullScreen: true }}

+ 1 - 0
src/components/BaseCrud/model.ts

@@ -16,6 +16,7 @@ export const CurdModel = model<Option>({
   },
 
   update(current: any) {
+    console.log('触发编辑');
     Store.set(SystemConst.BASE_CURD_MODEL, 'edit');
     Store.set(SystemConst.BASE_CURD_MODAL_VISIBLE, true);
     Store.set(SystemConst.BASE_CURD_CURRENT, current);

+ 9 - 1
src/components/SearchComponent/GroupNameControl.tsx

@@ -1,7 +1,13 @@
 import { ArrayItems } from '@formily/antd';
 import { Select } from 'antd';
 
-const GroupNameControl = (props: { name: string }) => {
+interface Props {
+  name?: string;
+  value: string;
+  onChange: () => void;
+}
+
+const GroupNameControl = (props: Props) => {
   const index = ArrayItems.useIndex!();
   return (
     <>
@@ -9,6 +15,8 @@ const GroupNameControl = (props: { name: string }) => {
         <div style={{ textAlign: 'center', fontWeight: 600 }}>{props?.name || '第一组'}</div>
       ) : (
         <Select
+          onChange={props.onChange}
+          value={props.value}
           options={[
             { label: '并且', value: 'and' },
             { label: '或者', value: 'or' },

+ 92 - 66
src/components/SearchComponent/index.tsx

@@ -14,12 +14,14 @@ import {
 import { createForm } from '@formily/core';
 import GroupNameControl from '@/components/SearchComponent/GroupNameControl';
 import { DeleteOutlined, DoubleRightOutlined } from '@ant-design/icons';
-import { Button, Dropdown, Menu, message, Popconfirm, Popover, Input as AInput } from 'antd';
+import { Button, Dropdown, Input as AInput, Menu, message, Popconfirm, Popover } from 'antd';
 import { useState } from 'react';
 import type { ProColumns } from '@jetlinks/pro-table';
 import type { EnumData } from '@/utils/typings';
 import styles from './index.less';
 import Service from '@/components/SearchComponent/service';
+import _ from 'lodash';
+import { useIntl } from '@@/plugin-locale/localeExports';
 
 const ui2Server = (source: SearchTermsUI): SearchTermsServer => [
   { terms: source.terms1, type: source.type },
@@ -35,7 +37,7 @@ const server2Ui = (source: SearchTermsServer): SearchTermsUI => ({
 interface Props<T> {
   field: ProColumns<T>[];
   onSearch: (params: any) => void;
-  target: string;
+  target?: string;
 }
 
 const termType = [
@@ -49,52 +51,43 @@ const termType = [
   { label: '<=', value: 'lte' },
   { label: '属于', value: 'in' },
   { label: '不属于', value: 'not in' },
-
-  // { label: '为空', value: '=\'\'' },
-  // { label: '不为空', value: '!=\'\'' },
-  // { label: 'isnull', value: 'is null' },
-  // { label: 'notnull', value: 'not null' },
-  // { label: '介于', value: 'between' },
-  // { label: '不介于', value: 'not between' },
 ];
 
 const service = new Service();
+const defaultTerm = { termType: 'like' };
+
 const SearchComponent = <T extends Record<string, any>>({ field, onSearch, target }: Props<T>) => {
+  const intl = useIntl();
   const [expand, setExpand] = useState<boolean>(true);
+  const initForm = server2Ui([{ terms: [defaultTerm], type: 'and' }, { terms: [defaultTerm] }]);
   const [logVisible, setLogVisible] = useState<boolean>(false);
   const [alias, setAlias] = useState<string>('');
   const [aliasVisible, setAliasVisible] = useState<boolean>(false);
-  const [initParams, setInitParams] = useState<SearchTermsServer>([
-    { terms: [{ column: null }], type: 'and' },
-    { terms: [{ column: null }] },
-  ]);
+  const [initParams, setInitParams] = useState<SearchTermsUI>(initForm);
   const [history, setHistory] = useState([]);
+
   const form = createForm<SearchTermsUI>({
     validateFirst: true,
-    initialValues: server2Ui(initParams),
+    initialValues: initParams,
   });
 
   const queryHistory = async () => {
-    const response = await service.history.query(target);
+    const response = await service.history.query(`${target}-search`);
     if (response.status === 200) {
       setHistory(response.result);
     }
   };
 
-  // useEffect(() => {
-  //   (queryHistory)();
-  // }, [target]);
-
   const handleExpand = () => {
     const value = form.values;
     if (!expand) {
       value.terms1.splice(1, 2);
       value.terms2.splice(1, 2);
     } else {
-      value.terms2.push({ column: null }, { column: null });
-      value.terms1.push({ column: null }, { column: null });
+      value.terms2.push(defaultTerm, defaultTerm);
+      value.terms1.push(defaultTerm, defaultTerm);
     }
-    setInitParams(ui2Server(value));
+    setInitParams(value);
     setExpand(!expand);
   };
   const SchemaField = createSchemaField({
@@ -123,7 +116,7 @@ const SearchComponent = <T extends Record<string, any>>({ field, onSearch, targe
     },
     'x-component': 'ArrayItems',
     type: 'array',
-    'x-value': new Array(expand ? 1 : 3).fill({ column: null }),
+    'x-value': new Array(expand ? 1 : 3).fill({ termType: 'like' }),
     items: {
       type: 'object',
       'x-component': 'FormGrid',
@@ -211,61 +204,87 @@ const SearchComponent = <T extends Record<string, any>>({ field, onSearch, targe
     },
   };
 
-  const menu = () => {
-    return (
-      <Menu>
-        {history.map((item: any) => (
-          <Menu.Item onClick={() => message.success(item.name)} key={item.id}>
-            <div
-              style={{
-                display: 'flex',
-                justifyContent: 'space-between',
-                alignItems: 'center',
+  const handleHistory = (item: SearchHistory) => {
+    const log = JSON.parse(item.content) as SearchTermsUI;
+    form.setValues(log);
+    setExpand(!(log.terms1?.length > 1 || log.terms2?.length > 1));
+    setInitParams(log);
+  };
+  const historyDom = (
+    <Menu>
+      {history.map((item: SearchHistory) => (
+        <Menu.Item onClick={() => handleHistory(item)} key={item.id}>
+          <div
+            style={{
+              display: 'flex',
+              justifyContent: 'space-between',
+              alignItems: 'center',
+            }}
+          >
+            <span style={{ marginRight: '5px' }}>{item.name}</span>
+            <Popconfirm
+              onConfirm={async () => {
+                const response = await service.history.remove(`${target}-search`, item.key);
+                if (response.status === 200) {
+                  message.success('操作成功');
+                  const temp = history.filter((h: any) => h.key !== item.key);
+                  setHistory(temp);
+                }
               }}
+              title={'确认删除吗?'}
             >
-              <span style={{ marginRight: '5px' }}>{item.name}</span>
-              <Popconfirm
-                onConfirm={async () => {
-                  const response = await service.history.remove(target, item.key);
-                  if (response.status === 200) {
-                    message.success('操作成功');
-                    const temp = history.filter((h: any) => h.key !== item.key);
-                    setHistory(temp);
-                  }
-                }}
-                title={'确认删除吗?'}
-              >
-                <DeleteOutlined />
-              </Popconfirm>
-            </div>
-          </Menu.Item>
-        ))}
-      </Menu>
-    );
-  };
+              <DeleteOutlined />
+            </Popconfirm>
+          </div>
+        </Menu.Item>
+      ))}
+    </Menu>
+  );
+
+  const formatValue = (value: SearchTermsUI): SearchTermsServer =>
+    ui2Server(value).map((term) => {
+      term.terms.map((item) => {
+        if (item.termType === 'like') {
+          item.value = `%${item.value}%`;
+          return item;
+        }
+        return item;
+      });
+      return term;
+    });
 
   const handleSearch = async () => {
-    const value = await form.submit<SearchTermsUI>();
-    // TODO
-    value.terms1 = value.terms1.filter((item) => item.column != null).filter((item) => item.value);
-    value.terms2 = value.terms2.filter((item) => item.column != null).filter((item) => item.value);
-    onSearch(value);
+    const value = form.values;
+    setInitParams(value);
+    const filterTerms = (data: Partial<Term>[]) =>
+      data.filter((item) => item.column != null).filter((item) => item.value);
+    const temp = _.cloneDeep(value);
+    temp.terms1 = filterTerms(temp.terms1);
+    temp.terms2 = filterTerms(temp.terms2);
+    onSearch(formatValue(temp));
   };
 
   const handleSaveLog = async () => {
     const value = await form.submit<SearchTermsUI>();
-    const response = await service.history.save(target, {
+    const response = await service.history.save(`${target}-search`, {
       name: alias,
-      content: JSON.stringify(ui2Server(value)),
+      content: JSON.stringify(value),
     });
     if (response.status === 200) {
       message.success('保存成功!');
     } else {
-      message.success('保存失败');
+      message.error('保存失败');
     }
     setAliasVisible(!aliasVisible);
   };
 
+  const resetForm = async () => {
+    const temp = initParams;
+    temp.terms1 = temp.terms1.map(() => defaultTerm);
+    temp.terms2 = temp.terms2.map(() => defaultTerm);
+    setInitParams(temp);
+    await form.reset();
+  };
   return (
     <div>
       <Form form={form} labelCol={4} wrapperCol={18}>
@@ -273,6 +292,7 @@ const SearchComponent = <T extends Record<string, any>>({ field, onSearch, targe
         <div className={styles.action}>
           <FormButtonGroup.FormItem labelCol={10} wrapperCol={14}>
             <Dropdown.Button
+              placement={'bottomLeft'}
               trigger={['click']}
               onClick={handleSearch}
               visible={logVisible}
@@ -280,11 +300,10 @@ const SearchComponent = <T extends Record<string, any>>({ field, onSearch, targe
                 setLogVisible(visible);
                 if (visible) {
                   await queryHistory();
-                  console.log('test');
                 }
               }}
               type="primary"
-              overlay={menu}
+              overlay={historyDom}
             >
               搜索
             </Dropdown.Button>
@@ -304,15 +323,22 @@ const SearchComponent = <T extends Record<string, any>>({ field, onSearch, targe
               visible={aliasVisible}
               onVisibleChange={(visible) => {
                 setAlias('');
-                setInitParams(ui2Server(form.values));
+                setInitParams(form.values);
                 setAliasVisible(visible);
               }}
               title="搜索名称"
               trigger="click"
             >
-              <Button block>保存</Button>
+              <Button block>
+                {intl.formatMessage({
+                  id: 'pages.data.option.save',
+                  defaultMessage: '保存',
+                })}
+              </Button>
             </Popover>
-            <Button block>重置</Button>
+            <Button block onClick={resetForm}>
+              重置
+            </Button>
           </FormButtonGroup.FormItem>
           <div>
             <DoubleRightOutlined

+ 10 - 0
src/components/SearchComponent/typings.d.ts

@@ -15,3 +15,13 @@ type SearchTermsServer = {
   terms: Partial<Term>[];
   type?: 'or' | 'and';
 }[];
+
+type SearchHistory = {
+  id: string;
+  key: string;
+  name: string;
+  type: string;
+  userId: string;
+  createTime: number;
+  content: string;
+};

+ 1 - 0
src/locales/en-US/pages.ts

@@ -27,6 +27,7 @@ export default {
   'pages.data.option.detail': 'Detail',
   'pages.data.option.download': 'Download',
   'pages.data.option.record': 'Records',
+  'pages.data.option.save': 'Save',
   'pages.searchTable.new': 'New',
   'pages.searchTable.titleStatus': 'Status',
   'pages.searchTable.titleStatus.all': 'All',

+ 3 - 0
src/locales/zh-CN/menu.ts

@@ -12,6 +12,9 @@ export default {
   'menu.system.permission': '权限管理',
   'menu.system.tenant': '租户管理',
   'menu.system.datasource': '数据源管理',
+  'menu.system.department': '部门管理',
+  'menu.system.assets': '资产分配',
+  'menu.system.member': '用户',
   'menu.device': '设备管理',
   'menu.device.product': '产品',
   'menu.device.product-detail': '产品详情',

+ 5 - 0
src/locales/zh-CN/pages.ts

@@ -27,6 +27,8 @@ export default {
   'pages.data.option.detail': '详情',
   'pages.data.option.download': '下载',
   'pages.data.option.record': '通知记录',
+  'pages.data.option.save': '保存',
+  'pages.data.option.assets': '资产分配',
   'pages.searchTable.new': '新建',
   'pages.searchTable.titleStatus': '状态',
   'pages.searchTable.titleStatus.all': '全部',
@@ -123,6 +125,9 @@ export default {
   'pages.system.org.option.bindUser': '绑定用户',
   'pages.system.org.option.permission': '权限分配',
   'pages.system.org.option.add': '添加下级',
+  // 系统设置-部门管理
+  'pages.system.department.user': '用户',
+  'pages.system.department.option.add': '新增子部门',
   // 系统设置-第三方平台
   'pages.system.openApi': '第三方平台',
   'pages.system.openApi.username': '用户名',

+ 141 - 0
src/pages/system/Department/Assets/deivce/bind.tsx

@@ -0,0 +1,141 @@
+// 资产-产品分类-绑定
+import type { ProColumns, ActionType } from '@jetlinks/pro-table';
+import ProTable from '@jetlinks/pro-table';
+import { DeviceBadge, service } from './index';
+import { Modal } from 'antd';
+import { useParams } from 'umi';
+import Models from './model';
+import { useEffect, useRef, useState } from 'react';
+import { observer } from '@formily/react';
+import { useIntl } from '@@/plugin-locale/localeExports';
+import type { DeviceItem } from '@/pages/system/Department/typings';
+import PermissionModal from '@/pages/system/Department/Assets/permissionModal';
+
+interface Props {
+  reload: () => void;
+  visible: boolean;
+  onCancel: () => void;
+}
+
+const Bind = observer((props: Props) => {
+  const intl = useIntl();
+  const param = useParams<{ id: string }>();
+  const actionRef = useRef<ActionType>();
+  const [perVisible, setPerVisible] = useState(false);
+
+  const columns: ProColumns<DeviceItem>[] = [
+    {
+      dataIndex: 'id',
+      title: 'ID',
+      width: 220,
+    },
+    {
+      dataIndex: 'name',
+      title: intl.formatMessage({
+        id: 'pages.table.name',
+        defaultMessage: '名称',
+      }),
+      search: {
+        transform: (value) => ({ name$LIKE: value }),
+      },
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.device.firmware.productName',
+        defaultMessage: '所属产品',
+      }),
+      dataIndex: 'configuration',
+      render: (_, row) => {
+        return row.productName;
+      },
+      search: false,
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.device.instance.registrationTime',
+        defaultMessage: '注册时间',
+      }),
+      dataIndex: 'registryTime',
+      search: false,
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.searchTable.titleStatus',
+        defaultMessage: '状态',
+      }),
+      dataIndex: 'state',
+      render: (_, row) => <DeviceBadge type={row.state.value} text={row.state.text} />,
+      search: false,
+    },
+  ];
+
+  const handleBind = () => {
+    if (Models.bindKeys.length) {
+      setPerVisible(true);
+    } else {
+      props.onCancel();
+    }
+  };
+
+  useEffect(() => {
+    if (props.visible) {
+      actionRef.current?.reload();
+    }
+  }, [props.visible]);
+
+  return (
+    <Modal
+      visible={props.visible}
+      onOk={handleBind}
+      onCancel={props.onCancel}
+      width={990}
+      title="绑定"
+    >
+      <PermissionModal
+        visible={perVisible}
+        type="device"
+        bindKeys={Models.bindKeys}
+        onCancel={(type) => {
+          setPerVisible(false);
+          if (type) {
+            props.reload();
+            props.onCancel();
+          }
+        }}
+      />
+      <ProTable<DeviceItem>
+        actionRef={actionRef}
+        columns={columns}
+        rowKey="id"
+        pagination={{
+          pageSize: 5,
+        }}
+        rowSelection={{
+          selectedRowKeys: Models.bindKeys,
+          onChange: (selectedRowKeys, selectedRows) => {
+            Models.bindKeys = selectedRows.map((item) => item.id);
+          },
+        }}
+        request={(params) => service.queryDeviceList(params)}
+        params={{
+          terms: [
+            {
+              column: 'id',
+              termType: 'dim-assets$not',
+              value: {
+                assetType: 'device',
+                targets: [
+                  {
+                    type: 'org',
+                    id: param.id,
+                  },
+                ],
+              },
+            },
+          ],
+        }}
+      />
+    </Modal>
+  );
+});
+export default Bind;

+ 274 - 0
src/pages/system/Department/Assets/deivce/index.tsx

@@ -0,0 +1,274 @@
+// 资产分配-产品分类
+import ProTable from '@jetlinks/pro-table';
+import type { ActionType, ProColumns } from '@jetlinks/pro-table';
+import { useIntl } from '@@/plugin-locale/localeExports';
+import { Button, message, Popconfirm, Tooltip, Badge } from 'antd';
+import { useRef, useState } from 'react';
+import { useParams } from 'umi';
+import { observer } from '@formily/react';
+import type { DeviceItem } from '@/pages/system/Department/typings';
+import { DisconnectOutlined, PlusOutlined } from '@ant-design/icons';
+import Models from './model';
+import Service from '@/pages/system/Department/Assets/service';
+import Bind from './bind';
+import SearchComponent from '@/components/SearchComponent';
+
+export const service = new Service<DeviceItem>();
+
+type DeviceBadgeProps = {
+  type: string;
+  text: string;
+};
+export const DeviceBadge = (props: DeviceBadgeProps) => {
+  const STATUS = {
+    notActive: 'processing',
+    offline: 'error',
+    online: 'success',
+  };
+  return <Badge status={STATUS[props.type]} text={props.text} />;
+};
+
+export default observer(() => {
+  const intl = useIntl();
+  const actionRef = useRef<ActionType>();
+
+  const param = useParams<{ id: string }>();
+  const [searchParam, setSearchParam] = useState({
+    terms: [
+      {
+        column: 'id',
+        termType: 'dim-assets',
+        value: {
+          assetType: 'device',
+          targets: [
+            {
+              type: 'org',
+              id: param.id,
+            },
+          ],
+        },
+      },
+    ],
+  });
+  /**
+   * 解除资产绑定
+   */
+  const handleUnBind = () => {
+    service
+      .unBind('device', [
+        {
+          targetType: 'org',
+          targetId: param.id,
+          assetType: 'device',
+          assetIdList: Models.unBindKeys,
+        },
+      ])
+      .subscribe({
+        next: () => message.success('操作成功'),
+        error: () => message.error('操作失败'),
+        complete: () => {
+          Models.unBindKeys = [];
+          actionRef.current?.reload();
+        },
+      });
+  };
+
+  const singleUnBind = (key: string) => {
+    Models.unBindKeys = [key];
+    handleUnBind();
+  };
+
+  const columns: ProColumns<DeviceItem>[] = [
+    {
+      dataIndex: 'id',
+      title: 'ID',
+      width: 220,
+    },
+    {
+      dataIndex: 'name',
+      title: intl.formatMessage({
+        id: 'pages.table.name',
+        defaultMessage: '名称',
+      }),
+      search: {
+        transform: (value) => ({ name$LIKE: value }),
+      },
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.device.firmware.productName',
+        defaultMessage: '所属产品',
+      }),
+      dataIndex: 'configuration',
+      render: (_, row) => {
+        return row.productName;
+      },
+      search: false,
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.device.instance.registrationTime',
+        defaultMessage: '注册时间',
+      }),
+      dataIndex: 'registryTime',
+      search: false,
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.searchTable.titleStatus',
+        defaultMessage: '状态',
+      }),
+      dataIndex: 'state',
+      filters: true,
+      onFilter: true,
+      valueType: 'select',
+      valueEnum: {
+        all: {
+          text: intl.formatMessage({
+            id: 'pages.searchTable.titleStatus.all',
+            defaultMessage: '全部',
+          }),
+          status: 'Default',
+        },
+        onLine: {
+          text: intl.formatMessage({
+            id: 'pages.device.instance.status.onLine',
+            defaultMessage: '在线',
+          }),
+          status: 'onLine',
+        },
+        offLine: {
+          text: intl.formatMessage({
+            id: 'pages.device.instance.status.offLine',
+            defaultMessage: '离线',
+          }),
+          status: 'offLine',
+        },
+        notActive: {
+          text: intl.formatMessage({
+            id: 'pages.device.instance.status.notActive',
+            defaultMessage: '未启用',
+          }),
+          status: 'notActive',
+        },
+      },
+      render: (_, row) => <DeviceBadge type={row.state.value} text={row.state.text} />,
+      search: false,
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.data.option',
+        defaultMessage: '操作',
+      }),
+      valueType: 'option',
+      align: 'center',
+      width: 200,
+      render: (text, record) => [
+        <Popconfirm
+          title={intl.formatMessage({
+            id: 'pages.system.role.option.unBindUser',
+            defaultMessage: '是否解除绑定',
+          })}
+          key="unBind"
+          onConfirm={() => {
+            singleUnBind(record.id);
+          }}
+        >
+          <a href="#">
+            <Tooltip
+              title={intl.formatMessage({
+                id: 'pages.system.role.option.unBindUser',
+                defaultMessage: '解除绑定',
+              })}
+            >
+              <DisconnectOutlined />
+            </Tooltip>
+          </a>
+        </Popconfirm>,
+      ],
+    },
+  ];
+
+  const closeModal = () => {
+    Models.bind = false;
+    Models.bindKeys = [];
+  };
+
+  return (
+    <>
+      <Bind
+        visible={Models.bind}
+        onCancel={closeModal}
+        reload={() => actionRef.current?.reload()}
+      />
+      <SearchComponent<DeviceItem>
+        field={columns}
+        onSearch={async (data) => {
+          setSearchParam({
+            terms: [
+              ...data,
+              {
+                column: 'id',
+                termType: 'dim-assets',
+                value: {
+                  assetType: 'device',
+                  targets: [
+                    {
+                      type: 'org',
+                      id: param.id,
+                    },
+                  ],
+                },
+              },
+            ],
+          });
+        }}
+        target="department-assets-device"
+      />
+      <ProTable<DeviceItem>
+        actionRef={actionRef}
+        columns={columns}
+        rowKey="id"
+        search={false}
+        params={searchParam}
+        request={(params) => service.queryDeviceList(params)}
+        rowSelection={{
+          selectedRowKeys: Models.unBindKeys,
+          onChange: (selectedRowKeys, selectedRows) => {
+            Models.unBindKeys = selectedRows.map((item) => item.id);
+          },
+        }}
+        toolBarRender={() => [
+          <Button
+            onClick={() => {
+              Models.bind = true;
+            }}
+            icon={<PlusOutlined />}
+            type="primary"
+            key="bind"
+          >
+            {intl.formatMessage({
+              id: 'pages.data.option.assets',
+              defaultMessage: '资产分配',
+            })}
+          </Button>,
+          <Popconfirm
+            title={intl.formatMessage({
+              id: 'pages.system.role.option.unBindUser',
+              defaultMessage: '是否批量解除绑定',
+            })}
+            key="unBind"
+            onConfirm={handleUnBind}
+          >
+            <Button icon={<DisconnectOutlined />} key="bind">
+              {intl.formatMessage({
+                id: 'pages.system.role.option.unBindUser',
+                defaultMessage: '批量解绑',
+              })}
+            </Button>
+          </Popconfirm>,
+        ]}
+      />
+    </>
+  );
+});

+ 16 - 0
src/pages/system/Department/Assets/deivce/model.ts

@@ -0,0 +1,16 @@
+// 用户数据模型
+import { model } from '@formily/reactive';
+
+type ModelType = {
+  bind: boolean;
+  bindKeys: string[];
+  unBindKeys: string[];
+};
+
+const Models = model<ModelType>({
+  bind: false,
+  bindKeys: [],
+  unBindKeys: [],
+});
+
+export default Models;

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

@@ -0,0 +1,55 @@
+// 部门-资产分配
+import { PageContainer } from '@ant-design/pro-layout';
+import { Tabs } from 'antd';
+import { useIntl } from '@@/plugin-locale/localeExports';
+import ProductCategory from './productCategory';
+import Product from './product';
+import Device from '@/pages/system/Department/Assets/deivce';
+
+// 资产类型
+const TabsArray = [
+  {
+    intlTitle: '1',
+    defaultMessage: '产品分类',
+    key: 'ProductCategory',
+    components: ProductCategory,
+  },
+  {
+    intlTitle: '2',
+    defaultMessage: '产品',
+    key: 'Product',
+    components: Product,
+  },
+  {
+    intlTitle: '3',
+    defaultMessage: '设备',
+    key: 'Device',
+    components: Device,
+  },
+];
+
+const Assets = () => {
+  const intl = useIntl();
+
+  return (
+    <PageContainer>
+      <div style={{ background: '#fff', padding: 12 }}>
+        <Tabs tabPosition="left" defaultActiveKey="ProductCategory">
+          {TabsArray.map((item) => (
+            <Tabs.TabPane
+              tab={intl.formatMessage({
+                id: item.intlTitle,
+                defaultMessage: item.defaultMessage,
+              })}
+              key={item.key}
+            >
+              <item.components />
+            </Tabs.TabPane>
+          ))}
+        </Tabs>
+      </div>
+    </PageContainer>
+  );
+};
+
+export default Assets;

+ 108 - 0
src/pages/system/Department/Assets/permissionModal.tsx

@@ -0,0 +1,108 @@
+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 type { ISchema } from '@formily/json-schema';
+import type { ModalProps } from 'antd/lib/modal/Modal';
+import { useParams } from 'umi';
+import Service from './service';
+
+type PermissionType = 'device' | 'product' | 'deviceCategory';
+
+export interface PerModalProps extends Omit<ModalProps, 'onOk' | 'onCancel'> {
+  type: PermissionType;
+  bindKeys: string[];
+  visible: boolean;
+  /**
+   * Model关闭事件
+   * @param type 是否为请求接口后关闭,用于外部table刷新数据
+   */
+  onCancel?: (type: boolean) => void;
+}
+
+const service = new Service('assets');
+
+export default (props: PerModalProps) => {
+  const intl = useIntl();
+  const params = useParams<{ id: string }>();
+
+  const SchemaField = createSchemaField({
+    components: {
+      Form,
+      FormItem,
+      Checkbox,
+    },
+  });
+
+  const form = createForm({
+    validateFirst: true,
+    initialValues: {},
+  });
+
+  /**
+   * 关闭Modal
+   * @param type 是否需要刷新外部table数据
+   */
+  const modalClose = (type: boolean) => {
+    if (typeof props.onCancel === 'function') {
+      props.onCancel(type);
+    }
+  };
+
+  const saveData = async () => {
+    const formData: any = await form.submit();
+    service
+      .bind(props.type, [
+        {
+          targetType: 'org',
+          targetId: params.id,
+          assetType: props.type,
+          assetIdList: props.bindKeys,
+          permission: formData.permission,
+        },
+      ])
+      .subscribe({
+        next: () => message.success('操作成功'),
+        error: () => message.error('操作失败'),
+        complete: () => {
+          modalClose(true);
+        },
+      });
+  };
+
+  const schema: ISchema = {
+    type: 'object',
+    properties: {
+      permission: {
+        type: 'array',
+        title: '资产权限',
+        'x-decorator': 'FormItem',
+        'x-component': 'Checkbox.Group',
+        enum: [
+          { label: '查看', value: 'read' },
+          { label: '编辑', value: 'save' },
+          { label: '删除', value: 'delete' },
+        ],
+      },
+    },
+  };
+
+  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}>
+        <SchemaField schema={schema} />
+      </Form>
+    </Modal>
+  );
+};

+ 123 - 0
src/pages/system/Department/Assets/product/bind.tsx

@@ -0,0 +1,123 @@
+// 资产-产品分类-绑定
+import type { ProColumns, ActionType } from '@jetlinks/pro-table';
+import ProTable from '@jetlinks/pro-table';
+import { service } from './index';
+import { Modal } from 'antd';
+import { useParams } from 'umi';
+import Models from './model';
+import { useEffect, useRef, useState } from 'react';
+import { observer } from '@formily/react';
+import { useIntl } from '@@/plugin-locale/localeExports';
+import type { ProductCategoryItem } from '@/pages/system/Department/typings';
+import PermissionModal from '@/pages/system/Department/Assets/permissionModal';
+
+interface Props {
+  reload: () => void;
+  visible: boolean;
+  onCancel: () => void;
+}
+
+const Bind = observer((props: Props) => {
+  const intl = useIntl();
+  const param = useParams<{ id: string }>();
+  const actionRef = useRef<ActionType>();
+  const [perVisible, setPerVisible] = useState(false);
+
+  const columns: ProColumns<ProductCategoryItem>[] = [
+    {
+      dataIndex: 'id',
+      title: 'ID',
+      width: 220,
+    },
+    {
+      dataIndex: 'name',
+      title: intl.formatMessage({
+        id: 'pages.table.name',
+        defaultMessage: '姓名',
+      }),
+      search: {
+        transform: (value) => ({ name$LIKE: value }),
+      },
+    },
+    {
+      dataIndex: 'username',
+      title: intl.formatMessage({
+        id: 'pages.table.describe',
+        defaultMessage: '用户名',
+      }),
+      search: {
+        transform: (value) => ({ username$LIKE: value }),
+      },
+    },
+  ];
+
+  const handleBind = () => {
+    if (Models.bindKeys.length) {
+      setPerVisible(true);
+    } else {
+      props.onCancel();
+    }
+  };
+
+  useEffect(() => {
+    if (props.visible) {
+      actionRef.current?.reload();
+    }
+  }, [props.visible]);
+
+  return (
+    <Modal
+      visible={props.visible}
+      onOk={handleBind}
+      onCancel={props.onCancel}
+      width={990}
+      title="绑定"
+    >
+      <PermissionModal
+        visible={perVisible}
+        type="product"
+        bindKeys={Models.bindKeys}
+        onCancel={(type) => {
+          setPerVisible(false);
+          if (type) {
+            props.reload();
+            props.onCancel();
+          }
+        }}
+      />
+      <ProTable<ProductCategoryItem>
+        actionRef={actionRef}
+        columns={columns}
+        rowKey="id"
+        pagination={{
+          pageSize: 5,
+        }}
+        rowSelection={{
+          selectedRowKeys: Models.bindKeys,
+          onChange: (selectedRowKeys, selectedRows) => {
+            Models.bindKeys = selectedRows.map((item) => item.id);
+          },
+        }}
+        request={(params) => service.queryProductList(params)}
+        params={{
+          terms: [
+            {
+              column: 'id',
+              termType: 'dim-assets$not',
+              value: {
+                assetType: 'product',
+                targets: [
+                  {
+                    type: 'org',
+                    id: param.id,
+                  },
+                ],
+              },
+            },
+          ],
+        }}
+      />
+    </Modal>
+  );
+});
+export default Bind;

+ 210 - 0
src/pages/system/Department/Assets/product/index.tsx

@@ -0,0 +1,210 @@
+// 资产分配-产品分类
+import ProTable from '@jetlinks/pro-table';
+import type { ActionType, ProColumns } 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 { observer } from '@formily/react';
+import type { ProductItem } from '@/pages/system/Department/typings';
+import { DisconnectOutlined, PlusOutlined } from '@ant-design/icons';
+import Service from '@/pages/system/Department/Assets/service';
+import Models from './model';
+import Bind from './bind';
+import SearchComponent from '@/components/SearchComponent';
+
+export const service = new Service<ProductItem>();
+
+export default observer(() => {
+  const intl = useIntl();
+  const actionRef = useRef<ActionType>();
+
+  const param = useParams<{ id: string }>();
+  const [searchParam, setSearchParam] = useState({
+    terms: [
+      {
+        column: 'id',
+        termType: 'dim-assets',
+        value: {
+          assetType: 'product',
+          targets: [
+            {
+              type: 'org',
+              id: param.id,
+            },
+          ],
+        },
+      },
+    ],
+  });
+
+  /**
+   * 解除资产绑定
+   */
+  const handleUnBind = () => {
+    service
+      .unBind('product', [
+        {
+          targetType: 'org',
+          targetId: param.id,
+          assetType: 'product',
+          assetIdList: Models.unBindKeys,
+        },
+      ])
+      .subscribe({
+        next: () => message.success('操作成功'),
+        error: () => message.error('操作失败'),
+        complete: () => {
+          Models.unBindKeys = [];
+          actionRef.current?.reload();
+        },
+      });
+  };
+
+  const singleUnBind = (key: string) => {
+    Models.unBindKeys = [key];
+    handleUnBind();
+  };
+
+  const columns: ProColumns<ProductItem>[] = [
+    {
+      dataIndex: 'id',
+      title: 'ID',
+      width: 220,
+    },
+    {
+      dataIndex: 'name',
+      title: intl.formatMessage({
+        id: 'pages.system.name',
+        defaultMessage: '名称',
+      }),
+      search: {
+        transform: (value) => ({ name$LIKE: value }),
+      },
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.system.tenant.memberManagement.administrators',
+        defaultMessage: '管理员',
+      }),
+      dataIndex: 'adminMember',
+      renderText: (text) => (text ? '是' : '否'),
+      search: false,
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.data.option',
+        defaultMessage: '操作',
+      }),
+      valueType: 'option',
+      align: 'center',
+      width: 200,
+      render: (text, record) => [
+        <Popconfirm
+          title={intl.formatMessage({
+            id: 'pages.system.role.option.unBindUser',
+            defaultMessage: '是否解除绑定',
+          })}
+          key="unBind"
+          onConfirm={() => {
+            singleUnBind(record.id);
+          }}
+        >
+          <a href="#">
+            <Tooltip
+              title={intl.formatMessage({
+                id: 'pages.system.role.option.unBindUser',
+                defaultMessage: '解除绑定',
+              })}
+            >
+              <DisconnectOutlined />
+            </Tooltip>
+          </a>
+        </Popconfirm>,
+      ],
+    },
+  ];
+
+  const closeModal = () => {
+    Models.bind = false;
+    Models.bindKeys = [];
+  };
+
+  return (
+    <>
+      <Bind
+        visible={Models.bind}
+        onCancel={closeModal}
+        reload={() => actionRef.current?.reload()}
+      />
+      <SearchComponent<ProductItem>
+        field={columns}
+        onSearch={async (data) => {
+          setSearchParam({
+            terms: [
+              ...data,
+              {
+                column: 'id',
+                termType: 'dim-assets',
+                value: {
+                  assetType: 'product',
+                  targets: [
+                    {
+                      type: 'org',
+                      id: param.id,
+                    },
+                  ],
+                },
+              },
+            ],
+          });
+        }}
+        target="department-assets-product"
+      />
+      <ProTable<ProductItem>
+        actionRef={actionRef}
+        columns={columns}
+        rowKey="id"
+        search={false}
+        params={searchParam}
+        request={(params) => service.queryProductList(params)}
+        rowSelection={{
+          selectedRowKeys: Models.unBindKeys,
+          onChange: (selectedRowKeys, selectedRows) => {
+            Models.unBindKeys = selectedRows.map((item) => item.id);
+          },
+        }}
+        toolBarRender={() => [
+          <Button
+            onClick={() => {
+              Models.bind = true;
+            }}
+            icon={<PlusOutlined />}
+            type="primary"
+            key="bind"
+          >
+            {intl.formatMessage({
+              id: 'pages.data.option.assets',
+              defaultMessage: '资产分配',
+            })}
+          </Button>,
+          <Popconfirm
+            title={intl.formatMessage({
+              id: 'pages.system.role.option.unBindUser',
+              defaultMessage: '是否批量解除绑定',
+            })}
+            key="unBind"
+            onConfirm={handleUnBind}
+          >
+            <Button icon={<DisconnectOutlined />} key="bind">
+              {intl.formatMessage({
+                id: 'pages.system.role.option.unBindUser',
+                defaultMessage: '批量解绑',
+              })}
+            </Button>
+          </Popconfirm>,
+        ]}
+      />
+    </>
+  );
+});

+ 16 - 0
src/pages/system/Department/Assets/product/model.ts

@@ -0,0 +1,16 @@
+// 用户数据模型
+import { model } from '@formily/reactive';
+
+type ModelType = {
+  bind: boolean;
+  bindKeys: string[];
+  unBindKeys: string[];
+};
+
+const Models = model<ModelType>({
+  bind: false,
+  bindKeys: [],
+  unBindKeys: [],
+});
+
+export default Models;

+ 131 - 0
src/pages/system/Department/Assets/productCategory/bind.tsx

@@ -0,0 +1,131 @@
+// 资产-产品分类-绑定
+import type { ProColumns, ActionType } from '@jetlinks/pro-table';
+import ProTable from '@jetlinks/pro-table';
+import { service, getTableKeys } from './index';
+import { Modal } from 'antd';
+import { useParams } from 'umi';
+import Models from './model';
+import { useRef, useState, useEffect } from 'react';
+import { observer } from '@formily/react';
+import { useIntl } from '@@/plugin-locale/localeExports';
+import type { ProductCategoryItem } from '@/pages/system/Department/typings';
+import PermissionModal from '@/pages/system/Department/Assets/permissionModal';
+
+interface Props {
+  reload: () => void;
+  visible: boolean;
+  onCancel: () => void;
+}
+
+const Bind = observer((props: Props) => {
+  const intl = useIntl();
+  const param = useParams<{ id: string }>();
+  const actionRef = useRef<ActionType>();
+  const [perVisible, setPerVisible] = useState(false);
+
+  const columns: ProColumns<ProductCategoryItem>[] = [
+    {
+      dataIndex: 'id',
+      title: 'ID',
+      width: 220,
+    },
+    {
+      dataIndex: 'key',
+      title: intl.formatMessage({
+        id: 'pages.system.name',
+        defaultMessage: '标识',
+      }),
+      search: {
+        transform: (value) => ({ name$LIKE: value }),
+      },
+    },
+    {
+      dataIndex: 'name',
+      title: intl.formatMessage({
+        id: 'pages.system.name',
+        defaultMessage: '分类名称',
+      }),
+      search: false,
+    },
+  ];
+
+  const handleBind = () => {
+    if (Models.bindKeys.length) {
+      setPerVisible(true);
+    } else {
+      props.onCancel();
+    }
+  };
+
+  useEffect(() => {
+    if (props.visible) {
+      actionRef.current?.reload();
+    }
+  }, [props.visible]);
+
+  return (
+    <Modal
+      visible={props.visible}
+      onOk={handleBind}
+      onCancel={props.onCancel}
+      width={990}
+      title="绑定"
+    >
+      <PermissionModal
+        visible={perVisible}
+        type="deviceCategory"
+        bindKeys={Models.bindKeys}
+        onCancel={(type) => {
+          setPerVisible(false);
+          if (type) {
+            props.reload();
+            props.onCancel();
+          }
+        }}
+      />
+      <ProTable<ProductCategoryItem>
+        actionRef={actionRef}
+        columns={columns}
+        rowKey="id"
+        pagination={false}
+        rowSelection={{
+          selectedRowKeys: Models.bindKeys,
+          onChange: (selectedRowKeys, selectedRows) => {
+            Models.bindKeys = getTableKeys(selectedRows);
+          },
+        }}
+        params={{
+          terms: [
+            {
+              column: 'id',
+              termType: 'dim-assets$not',
+              value: {
+                assetType: 'deviceCategory',
+                targets: [
+                  {
+                    type: 'org',
+                    id: param.id,
+                  },
+                ],
+              },
+            },
+          ],
+        }}
+        request={async (params) => {
+          const response = await service.queryProductCategoryList(params);
+          return {
+            code: response.message,
+            result: {
+              data: response.result,
+              pageIndex: 0,
+              pageSize: 0,
+              total: 0,
+            },
+            status: response.status,
+          };
+        }}
+      />
+    </Modal>
+  );
+});
+export default Bind;

+ 244 - 0
src/pages/system/Department/Assets/productCategory/index.tsx

@@ -0,0 +1,244 @@
+// 资产分配-产品分类
+import ProTable from '@jetlinks/pro-table';
+import type { ActionType, ProColumns } from '@jetlinks/pro-table';
+import { useIntl } from '@@/plugin-locale/localeExports';
+import { Button, Popconfirm, Tooltip, message } from 'antd';
+import { useRef, useState } from 'react';
+import { useParams } from 'umi';
+import { observer } from '@formily/react';
+import type { ProductCategoryItem } from '@/pages/system/Department/typings';
+import { DisconnectOutlined, PlusOutlined } from '@ant-design/icons';
+import Models from '@/pages/system/Department/Assets/productCategory/model';
+import Service from '@/pages/system/Department/Assets/service';
+import Bind from './bind';
+import SearchComponent from '@/components/SearchComponent';
+
+export const service = new Service<ProductCategoryItem>('assets');
+
+export const getTableKeys = (rows: ProductCategoryItem[]): string[] => {
+  let keys: string[] = [];
+  rows.forEach((item) => {
+    keys.push(item.id);
+    if (item.children && item.children.length) {
+      const childrenKeys = getTableKeys(item.children);
+      keys = [...keys, ...childrenKeys];
+    }
+  });
+  return keys;
+};
+
+export default observer(() => {
+  const intl = useIntl();
+  const actionRef = useRef<ActionType>();
+  const param = useParams<{ id: string }>();
+  const [searchParam, setSearchParam] = useState({
+    terms: [
+      {
+        column: 'id',
+        termType: 'dim-assets',
+        value: {
+          assetType: 'deviceCategory',
+          targets: [
+            {
+              type: 'org',
+              id: param.id,
+            },
+          ],
+        },
+      },
+    ],
+  });
+
+  /**
+   * 解除资产绑定
+   */
+  const handleUnBind = () => {
+    service
+      .unBind('deviceCategory', [
+        {
+          targetType: 'org',
+          targetId: param.id,
+          assetType: 'deviceCategory',
+          assetIdList: Models.unBindKeys,
+        },
+      ])
+      .subscribe({
+        next: () => message.success('操作成功'),
+        error: () => message.error('操作失败'),
+        complete: () => {
+          Models.unBindKeys = [];
+          actionRef.current?.reload();
+        },
+      });
+  };
+
+  const singleUnBind = (key: string) => {
+    Models.unBindKeys = [key];
+    handleUnBind();
+  };
+
+  const columns: ProColumns<ProductCategoryItem>[] = [
+    {
+      dataIndex: 'id',
+      title: 'ID',
+      width: 220,
+    },
+    {
+      dataIndex: 'key',
+      title: intl.formatMessage({
+        id: 'pages.device.category.key',
+        defaultMessage: '标识',
+      }),
+      search: {
+        transform: (value) => ({ name$LIKE: value }),
+      },
+    },
+    {
+      dataIndex: 'name',
+      title: intl.formatMessage({
+        id: 'pages.device.category.name',
+        defaultMessage: '分类名称',
+      }),
+      search: false,
+    },
+    {
+      dataIndex: 'description',
+      title: intl.formatMessage({
+        id: 'pages.system.description',
+        defaultMessage: '说明',
+      }),
+      search: false,
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.data.option',
+        defaultMessage: '操作',
+      }),
+      valueType: 'option',
+      align: 'center',
+      width: 200,
+      render: (text, record) => [
+        <Popconfirm
+          title={intl.formatMessage({
+            id: 'pages.system.role.option.unBindUser',
+            defaultMessage: '是否解除绑定',
+          })}
+          key="unBind"
+          onConfirm={() => {
+            singleUnBind(record.id);
+          }}
+        >
+          <a href="#">
+            <Tooltip
+              title={intl.formatMessage({
+                id: 'pages.system.role.option.unBindUser',
+                defaultMessage: '解除绑定',
+              })}
+            >
+              <DisconnectOutlined />
+            </Tooltip>
+          </a>
+        </Popconfirm>,
+      ],
+    },
+  ];
+
+  const closeModal = () => {
+    Models.bind = false;
+    Models.bindKeys = [];
+  };
+
+  return (
+    <>
+      <Bind
+        visible={Models.bind}
+        onCancel={closeModal}
+        reload={() => actionRef.current?.reload()}
+      />
+      <SearchComponent<ProductCategoryItem>
+        field={columns}
+        onSearch={async (data) => {
+          setSearchParam({
+            terms: [
+              ...data,
+              {
+                column: 'id',
+                termType: 'dim-assets',
+                value: {
+                  assetType: 'deviceCategory',
+                  targets: [
+                    {
+                      type: 'org',
+                      id: param.id,
+                    },
+                  ],
+                },
+              },
+            ],
+          });
+        }}
+        target="department-assets-category"
+      />
+      <ProTable<ProductCategoryItem>
+        actionRef={actionRef}
+        columns={columns}
+        params={searchParam}
+        search={false}
+        rowKey="id"
+        request={async (params) => {
+          const response = await service.queryProductCategoryList(params);
+          return {
+            code: response.message,
+            result: {
+              data: response.result,
+              pageIndex: 0,
+              pageSize: 0,
+              total: 0,
+            },
+            status: response.status,
+          };
+        }}
+        postData={(data) => {
+          console.log(data);
+          return data;
+        }}
+        rowSelection={{
+          selectedRowKeys: Models.unBindKeys,
+          onChange: (selectedRowKeys, selectedRows) => {
+            Models.unBindKeys = selectedRows.map((item) => item.id);
+          },
+        }}
+        toolBarRender={() => [
+          <Button
+            onClick={() => {
+              Models.bind = true;
+            }}
+            icon={<PlusOutlined />}
+            type="primary"
+            key="bind"
+          >
+            {intl.formatMessage({
+              id: 'pages.data.option.assets',
+              defaultMessage: '资产分配',
+            })}
+          </Button>,
+          <Popconfirm
+            title={intl.formatMessage({
+              id: 'pages.system.role.option.unBindUser',
+              defaultMessage: '是否批量解除绑定',
+            })}
+            key="unBind"
+            onConfirm={handleUnBind}
+          >
+            <Button icon={<DisconnectOutlined />} key="bind">
+              {intl.formatMessage({
+                id: 'pages.system.role.option.unBindUser',
+                defaultMessage: '批量解绑',
+              })}
+            </Button>
+          </Popconfirm>,
+        ]}
+      />
+    </>
+  );
+});

+ 16 - 0
src/pages/system/Department/Assets/productCategory/model.ts

@@ -0,0 +1,16 @@
+// 用户数据模型
+import { model } from '@formily/reactive';
+
+type ProductCategoryModelType = {
+  bind: boolean;
+  bindKeys: string[];
+  unBindKeys: string[];
+};
+
+const ProductCategoryModel = model<ProductCategoryModelType>({
+  bind: false,
+  bindKeys: [],
+  unBindKeys: [],
+});
+
+export default ProductCategoryModel;

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

@@ -0,0 +1,48 @@
+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';
+
+class Service<T> extends BaseService<T> {
+  // 资产绑定
+  bind = (type: string, params: any) =>
+    defer(() => from(request(`${this.uri}/bind/${type}`, { method: 'POST', data: params }))).pipe(
+      filter((item) => item.status === 200),
+      map((item) => item.result),
+    );
+
+  // 资产解绑
+  unBind = (type: string, params: any) =>
+    defer(() => from(request(`${this.uri}/unbind/${type}`, { method: 'POST', data: params }))).pipe(
+      filter((item) => item.status === 200),
+      map((item) => item.result),
+    );
+
+  // 资产-产品分类
+  queryProductCategoryList = (params: any) => {
+    return request(`${SystemConst.API_BASE}/device/category/_tree`, {
+      method: 'POST',
+      data: {
+        ...params,
+        paging: false,
+      },
+    });
+  };
+  // 资产-设备
+  queryDeviceList = (params: any) => {
+    return request<T>(`${SystemConst.API_BASE}/device/instance/_query`, {
+      method: 'POST',
+      data: params,
+    });
+  };
+  // 资产-产品
+  queryProductList = (params: any) => {
+    return request<T>(`${SystemConst.API_BASE}/device-product/_query`, {
+      method: 'POST',
+      data: params,
+    });
+  };
+}
+
+export default Service;

+ 98 - 0
src/pages/system/Department/Member/bind.tsx

@@ -0,0 +1,98 @@
+// 部门-用户绑定
+import type { ProColumns, ActionType } 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 } from 'react';
+import { useIntl } from '@@/plugin-locale/localeExports';
+
+interface Props {
+  reload: () => void;
+  visible: boolean;
+  onCancel: () => void;
+}
+
+const Bind = observer((props: Props) => {
+  const intl = useIntl();
+  const param = useParams<{ id: string }>();
+  const actionRef = useRef<ActionType>();
+
+  useEffect(() => {
+    if (props.visible) {
+      actionRef.current?.reload();
+    }
+  }, [props.visible]);
+
+  const columns: ProColumns<UserItem>[] = [
+    {
+      dataIndex: 'name',
+      title: intl.formatMessage({
+        id: 'pages.system.name',
+        defaultMessage: '姓名',
+      }),
+      search: {
+        transform: (value) => ({ name$LIKE: value }),
+      },
+    },
+    {
+      dataIndex: 'username',
+      title: intl.formatMessage({
+        id: 'pages.system.username',
+        defaultMessage: '用户名',
+      }),
+      search: {
+        transform: (value) => ({ username$LIKE: value }),
+      },
+    },
+  ];
+
+  const handleBind = () => {
+    if (MemberModel.bindUsers.length) {
+      service.handleUser(param.id, MemberModel.bindUsers, 'bind').subscribe({
+        next: () => message.success('操作成功'),
+        error: () => message.error('操作失败'),
+        complete: () => {
+          MemberModel.bindUsers = [];
+          actionRef.current?.reload();
+          props.reload();
+          props.onCancel();
+        },
+      });
+    } else {
+      props.onCancel();
+    }
+  };
+
+  return (
+    <Modal
+      visible={props.visible}
+      onOk={handleBind}
+      onCancel={props.onCancel}
+      width={990}
+      title="绑定"
+    >
+      <ProTable
+        actionRef={actionRef}
+        columns={columns}
+        rowKey="id"
+        pagination={{
+          pageSize: 5,
+        }}
+        rowSelection={{
+          selectedRowKeys: MemberModel.bindUsers,
+          onChange: (selectedRowKeys, selectedRows) => {
+            MemberModel.bindUsers = selectedRows.map((item) => item.id);
+          },
+        }}
+        request={(params) => service.queryUser(params)}
+        defaultParams={{
+          'id$in-dimension$org$not': param.id,
+        }}
+      />
+    </Modal>
+  );
+});
+export default Bind;

+ 214 - 0
src/pages/system/Department/Member/index.tsx

@@ -0,0 +1,214 @@
+// 部门-用户管理
+import { PageContainer } from '@ant-design/pro-layout';
+import ProTable from '@jetlinks/pro-table';
+import type { ActionType, ProColumns } from '@jetlinks/pro-table';
+import { useIntl } from '@@/plugin-locale/localeExports';
+import { Badge, Button, Card, Divider, message, Popconfirm, Tooltip } from 'antd';
+import { useRef, useState } from 'react';
+import { useParams } from 'umi';
+import { observer } from '@formily/react';
+import MemberModel from '@/pages/system/Department/Member/model';
+import type { MemberItem } from '@/pages/system/Department/typings';
+import Service from '@/pages/system/Department/Member/service';
+import { PlusOutlined, DisconnectOutlined } from '@ant-design/icons';
+import Bind from './bind';
+import SearchComponent from '@/components/SearchComponent';
+
+export const service = new Service('tenant');
+
+const Member = observer(() => {
+  const intl = useIntl();
+  const actionRef = useRef<ActionType>();
+
+  const param = useParams<{ id: string }>();
+  const [searchParam, setSearchParam] = useState({
+    terms: [{ column: 'id$in-dimension$org', value: param.id }],
+  });
+
+  const handleUnBind = () => {
+    service.handleUser(param.id, MemberModel.unBindUsers, 'unbind').subscribe({
+      next: () => message.success('操作成功'),
+      error: () => message.error('操作失败'),
+      complete: () => {
+        MemberModel.unBindUsers = [];
+        actionRef.current?.reload();
+      },
+    });
+  };
+
+  const singleUnBind = (key: string) => {
+    MemberModel.unBindUsers = [key];
+    handleUnBind();
+  };
+
+  const columns: ProColumns<MemberItem>[] = [
+    {
+      dataIndex: 'index',
+      valueType: 'indexBorder',
+      width: 48,
+    },
+    {
+      dataIndex: 'name',
+      title: intl.formatMessage({
+        id: 'pages.system.name',
+        defaultMessage: '姓名',
+      }),
+      search: {
+        transform: (value) => ({ name$LIKE: value }),
+      },
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.searchTable.titleStatus',
+        defaultMessage: '状态',
+      }),
+      dataIndex: 'status',
+      filters: true,
+      onFilter: true,
+      valueType: 'select',
+      valueEnum: {
+        all: {
+          text: intl.formatMessage({
+            id: 'pages.searchTable.titleStatus.all',
+            defaultMessage: '全部',
+          }),
+          status: 'Default',
+        },
+        1: {
+          text: intl.formatMessage({
+            id: 'pages.searchTable.titleStatus.normal',
+            defaultMessage: '正常',
+          }),
+          status: 1,
+        },
+        0: {
+          text: intl.formatMessage({
+            id: 'pages.searchTable.titleStatus.disable',
+            defaultMessage: '禁用',
+          }),
+          status: 0,
+        },
+      },
+      render: (value) => (
+        <Badge
+          status={value === 1 ? 'success' : 'error'}
+          text={
+            value === 1
+              ? intl.formatMessage({
+                  id: 'pages.searchTable.titleStatus.normal',
+                  defaultMessage: '正常',
+                })
+              : intl.formatMessage({
+                  id: 'pages.searchTable.titleStatus.disable',
+                  defaultMessage: '禁用',
+                })
+          }
+        />
+      ),
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.data.option',
+        defaultMessage: '操作',
+      }),
+      valueType: 'option',
+      align: 'center',
+      width: 200,
+      render: (text, record) => [
+        <Popconfirm
+          title={intl.formatMessage({
+            id: 'pages.system.role.option.unBindUser',
+            defaultMessage: '是否解除绑定',
+          })}
+          key="unBindUser"
+          onConfirm={() => {
+            singleUnBind(record.id);
+          }}
+        >
+          <a href="#">
+            <Tooltip
+              title={intl.formatMessage({
+                id: 'pages.system.role.option.unBindUser',
+                defaultMessage: '解除绑定',
+              })}
+            >
+              <DisconnectOutlined />
+            </Tooltip>
+          </a>
+        </Popconfirm>,
+      ],
+    },
+  ];
+
+  const closeModal = () => {
+    MemberModel.bind = false;
+  };
+
+  return (
+    <PageContainer>
+      <Bind
+        visible={MemberModel.bind}
+        onCancel={closeModal}
+        reload={() => actionRef.current?.reload()}
+      />
+      <Card>
+        <SearchComponent<MemberItem>
+          field={columns}
+          onSearch={async (data) => {
+            setSearchParam({
+              terms: [...data, { column: 'id$in-dimension$org', value: param.id }],
+            });
+          }}
+          target="department-user"
+        />
+      </Card>
+      <Divider />
+      <ProTable<MemberItem>
+        actionRef={actionRef}
+        columns={columns}
+        search={false}
+        rowKey="id"
+        request={(params) => service.queryUser(params)}
+        rowSelection={{
+          selectedRowKeys: MemberModel.unBindUsers,
+          onChange: (selectedRowKeys, selectedRows) => {
+            MemberModel.unBindUsers = selectedRows.map((item) => item.id);
+          },
+        }}
+        params={searchParam}
+        toolBarRender={() => [
+          <Button
+            onClick={() => {
+              MemberModel.bind = true;
+            }}
+            icon={<PlusOutlined />}
+            type="primary"
+            key="bind"
+          >
+            {intl.formatMessage({
+              id: 'pages.system.role.option.bindUser',
+              defaultMessage: '绑定用户',
+            })}
+          </Button>,
+          <Popconfirm
+            title={intl.formatMessage({
+              id: 'pages.system.role.option.unBindUser',
+              defaultMessage: '是否批量解除绑定',
+            })}
+            key="unBind"
+            onConfirm={handleUnBind}
+          >
+            <Button icon={<DisconnectOutlined />} key="bind">
+              {intl.formatMessage({
+                id: 'pages.system.role.option.unBindUser',
+                defaultMessage: '批量解绑',
+              })}
+            </Button>
+          </Popconfirm>,
+        ]}
+      />
+    </PageContainer>
+  );
+});
+
+export default Member;

+ 16 - 0
src/pages/system/Department/Member/model.ts

@@ -0,0 +1,16 @@
+// 用户数据模型
+import { model } from '@formily/reactive';
+
+type MemberModelType = {
+  bind: boolean;
+  bindUsers: string[];
+  unBindUsers: string[];
+};
+
+const MemberModel = model<MemberModelType>({
+  bind: false,
+  bindUsers: [],
+  unBindUsers: [],
+});
+
+export default MemberModel;

+ 29 - 0
src/pages/system/Department/Member/service.ts

@@ -0,0 +1,29 @@
+import BaseService from '@/utils/BaseService';
+import type { MemberItem } from '@/pages/system/Department/typings';
+import { defer, from } from 'rxjs';
+import { request } from '@@/plugin-request/request';
+import SystemConst from '@/utils/const';
+import { filter, map } from 'rxjs/operators';
+
+class Service extends BaseService<MemberItem> {
+  queryUser = (params: Record<string, unknown>) =>
+    request(`/${SystemConst.API_BASE}/user/_query`, {
+      method: 'POST',
+      data: params,
+    });
+
+  handleUser = (id: string, data: Record<string, unknown>[] | string[], type: 'bind' | 'unbind') =>
+    defer(() =>
+      from(
+        request(`/${SystemConst.API_BASE}/organization/${id}/users/_${type}`, {
+          method: 'POST',
+          data,
+        }),
+      ),
+    ).pipe(
+      filter((item) => item.status === 200),
+      map((item) => item.result),
+    );
+}
+
+export default Service;

+ 250 - 0
src/pages/system/Department/index.tsx

@@ -0,0 +1,250 @@
+// 部门管理
+import { PageContainer } from '@ant-design/pro-layout';
+import ProTable from '@jetlinks/pro-table';
+import type { ActionType, ProColumns } from '@jetlinks/pro-table';
+import { useRef, useState } from 'react';
+import { useIntl } from '@@/plugin-locale/localeExports';
+import { Button, message, Popconfirm, Tooltip, Card, Divider } from 'antd';
+import {
+  EditOutlined,
+  PlusOutlined,
+  PlusCircleOutlined,
+  TeamOutlined,
+  MedicineBoxOutlined,
+  DeleteOutlined,
+} from '@ant-design/icons';
+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 { Link } from 'umi';
+import Save from './save';
+import SearchComponent from '@/components/SearchComponent';
+
+export const service = new Service('organization');
+
+type ModelType = {
+  visible: boolean;
+  current: Partial<DepartmentItem>;
+  parentId: string | undefined;
+};
+export const State = model<ModelType>({
+  visible: false,
+  current: {},
+  parentId: undefined,
+});
+
+export default observer(() => {
+  const actionRef = useRef<ActionType>();
+  const intl = useIntl();
+  const [param, setParam] = useState({
+    terms: [{ column: 'typeId', value: 'org' }],
+  });
+
+  /**
+   * 根据部门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',
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.device.instanceDetail.detail.sort',
+        defaultMessage: '排序',
+      }),
+      search: false,
+      dataIndex: 'sortIndex',
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.data.option',
+        defaultMessage: '操作',
+      }),
+      valueType: 'option',
+      align: 'center',
+      width: 240,
+      render: (text, record) => [
+        <a
+          key="editable"
+          onClick={() => {
+            State.current = record;
+            State.visible = true;
+          }}
+        >
+          <Tooltip
+            title={intl.formatMessage({
+              id: 'pages.data.option.edit',
+              defaultMessage: '编辑',
+            })}
+          >
+            <EditOutlined />
+          </Tooltip>
+        </a>,
+        <a
+          key="editable"
+          onClick={() => {
+            State.current = {
+              parentId: record.id,
+            };
+            State.visible = true;
+          }}
+        >
+          <Tooltip
+            title={intl.formatMessage({
+              id: 'pages.system.department.option.add',
+              defaultMessage: '新增子部门',
+            })}
+          >
+            <PlusCircleOutlined />
+          </Tooltip>
+        </a>,
+        <Link key="assets" to={`/system/department/${record.id}/assets`}>
+          <Tooltip
+            title={intl.formatMessage({
+              id: 'pages.data.option.assets',
+              defaultMessage: '资产分配',
+            })}
+          >
+            <MedicineBoxOutlined />
+          </Tooltip>
+        </Link>,
+        <Link key="user" to={`/system/department/${record.id}/user`}>
+          <Tooltip
+            title={intl.formatMessage({
+              id: 'pages.system.department.user',
+              defaultMessage: '用户',
+            })}
+          >
+            <TeamOutlined />
+          </Tooltip>
+        </Link>,
+        <Popconfirm
+          key="unBindUser"
+          title={intl.formatMessage({
+            id: 'pages.system.role.option.unBindUser',
+            defaultMessage: '是否批量解除绑定',
+          })}
+          onConfirm={() => {
+            deleteItem(record.id);
+          }}
+        >
+          <Tooltip
+            title={intl.formatMessage({
+              id: 'pages.data.option.delete',
+              defaultMessage: '删除',
+            })}
+          >
+            <a key="delete">
+              <DeleteOutlined />
+            </a>
+          </Tooltip>
+        </Popconfirm>,
+      ],
+    },
+  ];
+
+  const schema: ISchema = {
+    type: 'object',
+    properties: {
+      name: {
+        type: 'string',
+        title: '名称',
+        required: true,
+        'x-decorator': 'FormItem',
+        'x-component': 'Input',
+      },
+      sortIndex: {
+        type: 'string',
+        title: '排序',
+        'x-decorator': 'FormItem',
+        'x-component': 'Input',
+      },
+    },
+  };
+
+  return (
+    <PageContainer>
+      <Card>
+        <SearchComponent<DepartmentItem>
+          field={columns}
+          onSearch={async (data) => {
+            setParam({ terms: [...data, { column: 'typeId', value: 'org' }] });
+          }}
+          target="department"
+        />
+      </Card>
+      <Divider />
+      <ProTable<DepartmentItem>
+        columns={columns}
+        actionRef={actionRef}
+        request={async (params) => {
+          const response = await service.queryOrgThree({ paging: false, ...params });
+          return {
+            code: response.message,
+            result: {
+              data: response.result,
+              pageIndex: 0,
+              pageSize: 0,
+              total: 0,
+            },
+            status: response.status,
+          };
+        }}
+        rowKey="id"
+        pagination={false}
+        search={false}
+        params={param}
+        toolBarRender={() => [
+          <Button
+            onClick={() => (State.visible = true)}
+            key="button"
+            icon={<PlusOutlined />}
+            type="primary"
+          >
+            {intl.formatMessage({
+              id: 'pages.data.option.add',
+              defaultMessage: '新增',
+            })}
+          </Button>,
+        ]}
+        headerTitle={intl.formatMessage({
+          id: 'pages.system.department',
+          defaultMessage: '部门列表',
+        })}
+      />
+      <Save<DepartmentItem>
+        service={service}
+        onCancel={(type) => {
+          if (type) {
+            actionRef.current?.reload();
+          }
+          State.current = {};
+          State.visible = false;
+        }}
+        data={State.current}
+        visible={State.visible}
+        schema={schema}
+      />
+    </PageContainer>
+  );
+});

+ 117 - 0
src/pages/system/Department/save.tsx

@@ -0,0 +1,117 @@
+// Modal 弹窗,用于新增、修改数据
+import React from 'react';
+import { createForm } from '@formily/core';
+import { createSchemaField } from '@formily/react';
+import {
+  ArrayTable,
+  Editable,
+  Form,
+  FormGrid,
+  FormItem,
+  FormTab,
+  Input,
+  NumberPicker,
+  Password,
+  Select,
+  Switch,
+  Upload,
+  Checkbox,
+} from '@formily/antd';
+import { message, Modal } from 'antd';
+import { useIntl } from '@@/plugin-locale/localeExports';
+import type { ISchema } from '@formily/json-schema';
+import type { ModalProps } from 'antd/lib/modal/Modal';
+import FUpload from '@/components/Upload';
+import * as ICONS from '@ant-design/icons';
+import type BaseService from '@/utils/BaseService';
+
+export interface SaveModalProps<T> extends Omit<ModalProps, 'onOk' | 'onCancel'> {
+  service: BaseService<T>;
+  data?: Partial<T>;
+  /**
+   * Model关闭事件
+   * @param type 是否为请求接口后关闭,用于外部table刷新数据
+   */
+  onCancel?: (type: boolean) => void;
+  schema: ISchema;
+}
+
+const Save = <T extends object>(props: SaveModalProps<T>) => {
+  const { data, schema, onCancel, service } = props;
+  const intl = useIntl();
+
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      FormTab,
+      Input,
+      Password,
+      Upload,
+      Select,
+      ArrayTable,
+      Switch,
+      FormGrid,
+      Editable,
+      NumberPicker,
+      FUpload,
+      Checkbox,
+    },
+    scope: {
+      icon(name: any) {
+        return React.createElement(ICONS[name]);
+      },
+    },
+  });
+
+  const form = createForm({
+    validateFirst: true,
+    initialValues: data || {},
+  });
+
+  /**
+   * 关闭Modal
+   * @param type 是否需要刷新外部table数据
+   */
+  const modalClose = (type: boolean) => {
+    if (typeof onCancel === 'function') {
+      onCancel(type);
+    }
+  };
+
+  /**
+   * 新增、修改数据
+   */
+  const saveData = async () => {
+    const formData: T = await form.submit();
+
+    const response =
+      data && 'id' in data ? await service.update(formData) : await service.save(formData);
+
+    if (response.status === 200) {
+      message.success('操作成功!');
+      modalClose(true);
+    } else {
+      message.error('操作成功!');
+    }
+  };
+
+  return (
+    <Modal
+      title={intl.formatMessage({
+        id: `pages.data.option.${data && 'id' in data ? 'edit' : 'add'}`,
+        defaultMessage: '新增',
+      })}
+      visible={props.visible}
+      onOk={saveData}
+      onCancel={() => {
+        modalClose(false);
+      }}
+    >
+      <Form form={form} labelCol={5} wrapperCol={16}>
+        <SchemaField schema={schema} />
+      </Form>
+    </Modal>
+  );
+};
+
+export default Save;

+ 11 - 0
src/pages/system/Department/service.ts

@@ -0,0 +1,11 @@
+import BaseService from '@/utils/BaseService';
+import type { DepartmentItem } from '@/pages/system/Department/typings';
+import { request } from '@@/plugin-request/request';
+
+class Service extends BaseService<DepartmentItem> {
+  // 根据ID查询部门
+  queryOrgThree = (params: any) =>
+    request(`${this.uri}/_all/tree`, { method: 'POST', data: params });
+}
+
+export default Service;

+ 42 - 0
src/pages/system/Department/typings.d.ts

@@ -0,0 +1,42 @@
+import type { State } from '@/utils/typings';
+
+// 部门
+export type DepartmentItem = {
+  id: string;
+  name: string;
+  path: string;
+  sortIndex: number;
+  level: number;
+  code: string;
+  parentId: string;
+  children: DepartmentItem[];
+};
+
+// 用户
+export type MemberItem = {
+  id: string;
+  name: string;
+  username: string;
+  status: number;
+  createTime: number;
+  creatorId: string;
+};
+
+// 产品
+export type ProductItem = {
+  id: string;
+  name: string;
+  description: string;
+};
+
+// 产品分类
+export type ProductCategoryItem = { key: string; children: ProductCategoryItem[] } & ProductItem;
+
+// 设备
+export type DeviceItem = {
+  id: string;
+  name: string;
+  productName: string;
+  createTime: string;
+  state: State;
+};

+ 5 - 5
src/pages/system/OpenAPI/index.tsx

@@ -63,29 +63,29 @@ const OpenAPI: React.FC = observer(() => {
       valueType: 'select',
       hideInForm: true,
       onFilter: true,
-      valueEnum: [
-        {
+      valueEnum: {
+        default: {
           text: intl.formatMessage({
             id: 'pages.searchTable.titleStatus.all',
             defaultMessage: '全部',
           }),
           status: 'Default',
         },
-        {
+        '1': {
           text: intl.formatMessage({
             id: 'pages.searchTable.titleStatus.normal',
             defaultMessage: '正常',
           }),
           status: '1',
         },
-        {
+        '0': {
           text: intl.formatMessage({
             id: 'pages.searchTable.titleStatus.disable',
             defaultMessage: '禁用',
           }),
           status: '0',
         },
-      ],
+      },
     },
     {
       title: intl.formatMessage({

+ 0 - 59
src/pages/system/Org/NodeTemplate/index.tsx

@@ -1,59 +0,0 @@
-import styles from '../index.less';
-import { Avatar, Dropdown } from 'antd';
-import { SmallDashOutlined, UserOutlined } from '@ant-design/icons';
-import React from 'react';
-import type { OrgItem } from '@/pages/system/Org/typings';
-import { useIntl } from '@@/plugin-locale/localeExports';
-
-declare type OverlayFunc = () => React.ReactElement;
-
-interface Props {
-  data: Partial<OrgItem>;
-  action: React.ReactElement | OverlayFunc;
-}
-
-const NodeTemplate: React.FC<Props> = (props) => {
-  const intl = useIntl();
-  const { data, action } = props;
-  return (
-    <div className={styles.node}>
-      <div className={styles.top}>
-        <span className={styles.title}>{data.name}</span>
-        <Avatar size="small" icon={<UserOutlined />} />
-      </div>
-
-      <div className={styles.content}>
-        <div className={styles.item}>
-          {data.code !== null && (
-            <div>
-              <span className={styles.mark}>
-                {intl.formatMessage({
-                  id: 'pages.system.org.encoding',
-                  defaultMessage: '编码',
-                })}
-              </span>
-              <span>{data.code}</span>
-            </div>
-          )}
-          <div>
-            <span className={styles.mark}>
-              {intl.formatMessage({
-                id: 'pages.system.org.count',
-                defaultMessage: '下级数量',
-              })}
-            </span>
-            <span>{data?.children?.length || 0}</span>
-          </div>
-        </div>
-        <div className={styles.action}>
-          <Dropdown overlay={action}>
-            <a className="ant-dropdown-link" onClick={(e) => e.preventDefault()}>
-              <SmallDashOutlined />
-            </a>
-          </Dropdown>
-        </div>
-      </div>
-    </div>
-  );
-};
-export default NodeTemplate;

+ 0 - 101
src/pages/system/Org/Save/index.tsx

@@ -1,101 +0,0 @@
-import { message, Modal } from 'antd';
-import { createForm } from '@formily/core';
-import { createSchemaField } from '@formily/react';
-import { NumberPicker, Form, Input, FormItem } from '@formily/antd';
-import { useIntl } from '@@/plugin-locale/localeExports';
-import OrgModel from '@/pages/system/Org/model';
-import { service } from '@/pages/system/Org';
-
-interface Props {
-  refresh?: () => void;
-  visible: boolean;
-}
-
-const Save = (props: Props) => {
-  const intl = useIntl();
-  const form = createForm({
-    initialValues: OrgModel.current,
-  });
-
-  const SchemaField = createSchemaField({
-    components: {
-      FormItem,
-      Input,
-      NumberPicker,
-    },
-  });
-
-  const schema = {
-    type: 'object',
-    properties: {
-      code: {
-        title: intl.formatMessage({
-          id: 'pages.system.org.encoding',
-          defaultMessage: '编码',
-        }),
-        type: 'string',
-        'x-decorator': 'FormItem',
-        'x-component': 'Input',
-        name: 'id',
-        required: true,
-      },
-      name: {
-        title: intl.formatMessage({
-          id: 'pages.table.name',
-          defaultMessage: '名称',
-        }),
-        type: 'string',
-        'x-decorator': 'FormItem',
-        'x-component': 'Input',
-        name: 'name',
-        required: true,
-      },
-      sortIndex: {
-        title: intl.formatMessage({
-          id: 'pages.system.org.add.orderNumber',
-          defaultMessage: '序号',
-        }),
-        type: 'string',
-        'x-decorator': 'FormItem',
-        'x-component': 'NumberPicker',
-        name: 'name',
-        required: true,
-      },
-      describe: {
-        title: intl.formatMessage({
-          id: 'pages.table.describe',
-          defaultMessage: '描述',
-        }),
-        type: 'string',
-        'x-decorator': 'FormItem',
-        'x-component': 'Input.TextArea',
-        name: 'describe',
-      },
-    },
-  };
-
-  const save = async () => {
-    const data: Record<string, unknown> = await form.submit!();
-    await service.update({ ...data, parentId: OrgModel.parentId });
-    message.success('保存成功');
-    OrgModel.closeEdit();
-    props.refresh?.();
-  };
-
-  return (
-    <Modal
-      onOk={() => save()}
-      title={`${OrgModel.parentId ? '添加下级' : '编辑'}`}
-      visible={props.visible}
-      onCancel={() => {
-        OrgModel.closeEdit();
-        props.refresh?.();
-      }}
-    >
-      <Form form={form} labelCol={5} wrapperCol={16}>
-        <SchemaField schema={schema} />
-      </Form>
-    </Modal>
-  );
-};
-export default Save;

+ 0 - 70
src/pages/system/Org/index.less

@@ -1,70 +0,0 @@
-.orgContainer {
-  :global {
-    .orgchart {
-      background: #fff;
-    }
-    .orgchart-container {
-      border: none;
-    }
-  }
-}
-
-.node {
-  display: flex;
-  flex-direction: column;
-  min-width: 140px;
-  border: 1px solid #4c77bf;
-  border-radius: 3px;
-  transition: 0.3s;
-}
-
-.node:hover {
-  box-shadow: 0 0 10px 10px #b1d9ff;
-}
-
-.top {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  height: 36px;
-  padding: 0 12px;
-  color: #fff;
-  font-size: 14px;
-  background: #4c77bf;
-}
-
-.title {
-  margin-right: 12px;
-  white-space: nowrap;
-}
-
-.content {
-  display: flex;
-  flex: 1 1;
-  align-items: center;
-  justify-content: space-between;
-  padding: 3px 12px;
-  background: #fff;
-}
-
-.item > div {
-  display: flex;
-}
-
-.mark {
-  display: flex;
-  justify-content: flex-start;
-  width: 50px;
-  margin-right: 10px;
-  color: rgba(0, 0, 0, 0.45);
-  font-size: 14px;
-  white-space: nowrap;
-}
-
-.action {
-  align-self: flex-end;
-  height: 100%;
-  padding-bottom: 3px;
-  vertical-align: bottom;
-  cursor: pointer;
-}

+ 0 - 179
src/pages/system/Org/index.tsx

@@ -1,179 +0,0 @@
-import { PageContainer } from '@ant-design/pro-layout';
-import OrganizationChart from '@dabeng/react-orgchart';
-import styles from './index.less';
-import { Drawer, Menu, message, Modal } from 'antd';
-import NodeTemplate from '@/pages/system/Org/NodeTemplate';
-import { observer } from '@formily/react';
-import { useEffect } from 'react';
-import Service from '@/pages/system/Org/service';
-import encodeQuery from '@/utils/encodeQuery';
-import Save from '@/pages/system/Org/Save';
-import { useIntl } from '@@/plugin-locale/localeExports';
-import autzModel from '@/components/Authorization/autz';
-import Authorization from '@/components/Authorization';
-import { BindModel } from '@/components/BindUser/model';
-import BindUser from '@/components/BindUser';
-import OrgModel from '@/pages/system/Org/model';
-
-export const service = new Service('organization');
-const Org = observer(() => {
-  const intl = useIntl();
-  const hitCenter = () => {
-    const orgChart = document.getElementsByClassName('orgchart-container')[0];
-    const { width } = orgChart.getBoundingClientRect();
-    orgChart.scrollLeft = width;
-  };
-
-  const query = async () => {
-    const response = await service.queryTree(
-      encodeQuery({
-        paging: false,
-        terms: { typeId: 'org' },
-      }),
-    );
-    OrgModel.data = {
-      id: null,
-      name: intl.formatMessage({
-        id: 'pages.system.org',
-        defaultMessage: '机构管理',
-      }),
-      title: '组织架构',
-      children: response.result,
-    };
-    hitCenter();
-    return OrgModel;
-  };
-
-  const remove = async (id: string) => {
-    await service.remove(id);
-    await query();
-    message.success(
-      intl.formatMessage({
-        id: 'pages.data.option.success',
-        defaultMessage: '操作成功!',
-      }),
-    );
-  };
-  useEffect(() => {
-    query();
-  }, []);
-
-  const menu = (nodeData: any) => {
-    const addNext = (
-      <Menu.Item key="addNext">
-        <a key="addNext" onClick={() => OrgModel.addNext(nodeData)}>
-          {intl.formatMessage({
-            id: 'pages.system.org.option.add',
-            defaultMessage: '添加下级',
-          })}
-        </a>
-      </Menu.Item>
-    );
-    return nodeData.id === null ? (
-      <Menu>{addNext}</Menu>
-    ) : (
-      <Menu>
-        <Menu.Item key="edit">
-          <a
-            key="edit"
-            onClick={() => {
-              OrgModel.update(nodeData);
-            }}
-          >
-            {intl.formatMessage({
-              id: 'pages.data.option.edit',
-              defaultMessage: '编辑',
-            })}
-          </a>
-        </Menu.Item>
-        {addNext}
-        <Menu.Item key="autz">
-          <a
-            key="autz"
-            onClick={() => {
-              autzModel.autzTarget.id = nodeData.id;
-              autzModel.autzTarget.name = nodeData.name;
-              autzModel.visible = true;
-            }}
-          >
-            {intl.formatMessage({
-              id: 'pages.system.org.option.permission',
-              defaultMessage: '权限分配',
-            })}
-          </a>
-        </Menu.Item>
-        <Menu.Item key="bindUser">
-          <a
-            key="bindUser"
-            onClick={() => {
-              BindModel.dimension = {
-                id: nodeData.id,
-                name: nodeData.name,
-                type: 'org',
-              };
-              BindModel.visible = true;
-            }}
-          >
-            {intl.formatMessage({
-              id: 'pages.system.org.option.bindUser',
-              defaultMessage: '绑定用户',
-            })}
-          </a>
-        </Menu.Item>
-        <Menu.Item key="delete">
-          <a key="delete" onClick={() => remove(nodeData.id)}>
-            {intl.formatMessage({
-              id: 'pages.data.option.remove',
-              defaultMessage: '删除',
-            })}
-          </a>
-        </Menu.Item>
-      </Menu>
-    );
-  };
-  return (
-    <PageContainer>
-      <div className={styles.orgContainer}>
-        <OrganizationChart
-          datasource={OrgModel.data}
-          pan={true}
-          NodeTemplate={(nodeData: any) => (
-            <NodeTemplate data={nodeData.nodeData} action={menu(nodeData.nodeData)} />
-          )}
-        />
-      </div>
-      <Save refresh={query} visible={OrgModel.edit} />
-      <Modal
-        visible={BindModel.visible}
-        closable={false}
-        onCancel={() => {
-          BindModel.visible = false;
-          BindModel.bind = false;
-        }}
-        width={BindModel.bind ? '90vw' : '60vw'}
-      >
-        <BindUser />
-      </Modal>
-      <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}
-        />
-      </Drawer>
-    </PageContainer>
-  );
-});
-
-export default Org;

+ 0 - 33
src/pages/system/Org/model.ts

@@ -1,33 +0,0 @@
-import { model } from '@formily/reactive';
-import type { OrgItem, OrgModelType } from '@/pages/system/Org/typings';
-
-const OrgModel = model<OrgModelType>({
-  edit: false,
-  parentId: '',
-  data: {},
-  current: {},
-  authorize: true,
-
-  update(data: Partial<OrgItem>) {
-    this.current = data;
-    this.edit = true;
-    this.parentId = undefined;
-  },
-
-  addNext(parentData: Partial<OrgItem>) {
-    this.parentId = parentData.id;
-    this.edit = true;
-    this.current = {};
-  },
-
-  authorized(data: Partial<OrgItem>) {
-    this.current = data;
-    this.authorize = true;
-  },
-  closeEdit() {
-    this.current = {};
-    this.edit = false;
-  },
-});
-
-export default OrgModel;

+ 0 - 11
src/pages/system/Org/service.ts

@@ -1,11 +0,0 @@
-import BaseService from '@/utils/BaseService';
-import type { OrgItem } from '@/pages/system/Org/typings';
-import { request } from '@@/plugin-request/request';
-
-class Service extends BaseService<OrgItem> {
-  queryTree(params: any): Promise<any> {
-    return request(`${this.uri}/_all/tree`, { params, method: 'GET' });
-  }
-}
-
-export default Service;

+ 0 - 32
src/pages/system/Org/typings.d.ts

@@ -1,32 +0,0 @@
-export type OrgItem = {
-  id: string;
-  parentId: string | undefined;
-  path: string;
-  sortIndex: number;
-  level: number;
-  name: string;
-  describe: string;
-  permissionExpresion: string;
-  url: string;
-  icon: string;
-  status: number;
-  code?: string;
-  children?: OrgItem[];
-};
-
-export type OrgModelType = {
-  data: Partial<{
-    id: null;
-    name: string;
-    title: string;
-    children: Partial<OrgItem>[];
-  }>;
-  current: Partial<OrgItem>;
-  parentId: string | undefined;
-  edit: boolean;
-  update: (data: Partial<OrgItem>) => void;
-  addNext: (parentData: Partial<OrgItem>) => void;
-  authorize: boolean;
-  authorized: (data: Partial<OrgItem>) => void;
-  closeEdit: () => void;
-};

+ 2 - 11
src/pages/system/Permission/index.tsx

@@ -6,7 +6,7 @@ import {
   PlayCircleOutlined,
   MinusOutlined,
 } from '@ant-design/icons';
-import { Menu, Tooltip, Popconfirm, message, Card, Button, Upload } from 'antd';
+import { Menu, Tooltip, Popconfirm, message, Button, Upload } from 'antd';
 import type { ProColumns, ActionType } from '@jetlinks/pro-table';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import BaseCrud from '@/components/BaseCrud';
@@ -17,7 +17,6 @@ import type { ISchema } from '@formily/json-schema';
 import Service from '@/pages/system/Permission/service';
 import { model } from '@formily/reactive';
 import { observer } from '@formily/react';
-import SearchComponent from '@/components/SearchComponent';
 import moment from 'moment';
 import SystemConst from '@/utils/const';
 import Token from '@/utils/token';
@@ -461,16 +460,8 @@ const Permission: React.FC = observer(() => {
   };
   return (
     <PageContainer>
-      <Card style={{ marginBottom: '20px' }}>
-        <SearchComponent<PermissionItem>
-          field={columns}
-          onSearch={async (data) => {
-            message.success(JSON.stringify(data));
-          }}
-          target="permission-search"
-        />
-      </Card>
       <BaseCrud<PermissionItem>
+        moduleName="permission"
         actionRef={actionRef}
         columns={columns}
         service={service}

+ 2 - 12
src/pages/system/Role/index.tsx

@@ -1,7 +1,7 @@
 import { PageContainer } from '@ant-design/pro-layout';
 import React, { useRef } from 'react';
 import { EditOutlined, MinusOutlined } from '@ant-design/icons';
-import { Card, message, Popconfirm, Tooltip } from 'antd';
+import { message, Popconfirm, Tooltip } from 'antd';
 import type { ProColumns, ActionType } from '@jetlinks/pro-table';
 import BaseCrud from '@/components/BaseCrud';
 import BaseService from '@/utils/BaseService';
@@ -12,7 +12,6 @@ import { observer } from '@formily/react';
 // import { BindModel } from '@/components/BindUser/model';
 // import BindUser from '@/components/BindUser';
 import { Link } from 'umi';
-import SearchComponent from '@/components/SearchComponent';
 
 export const service = new BaseService<RoleItem>('role');
 
@@ -159,20 +158,11 @@ const Role: React.FC = observer(() => {
       },
     },
   };
-
   return (
     <PageContainer>
-      <Card style={{ marginBottom: '20px' }}>
-        <SearchComponent<RoleItem>
-          field={columns}
-          onSearch={async (data) => {
-            message.success(JSON.stringify(data));
-          }}
-          target="role-search"
-        />
-      </Card>
       <BaseCrud<RoleItem>
         actionRef={actionRef}
+        moduleName="role"
         columns={columns}
         service={service}
         search={false}

+ 5 - 5
src/pages/system/Tenant/index.tsx

@@ -70,29 +70,29 @@ const Tenant = observer(() => {
       hideInForm: true,
       onFilter: true,
       search: false,
-      valueEnum: [
-        {
+      valueEnum: {
+        default: {
           text: intl.formatMessage({
             id: 'pages.searchTable.titleStatus.all',
             defaultMessage: '全部',
           }),
           status: 'Default',
         },
-        {
+        '1': {
           text: intl.formatMessage({
             id: 'pages.searchTable.titleStatus.normal',
             defaultMessage: '正常',
           }),
           status: '1',
         },
-        {
+        '0': {
           text: intl.formatMessage({
             id: 'pages.searchTable.titleStatus.disable',
             defaultMessage: '禁用',
           }),
           status: '0',
         },
-      ],
+      },
     },
     {
       title: intl.formatMessage({

+ 258 - 0
src/pages/system/User/Save/index.tsx

@@ -0,0 +1,258 @@
+import { message, Modal } from 'antd';
+import { useIntl } from 'umi';
+import type { Field } from '@formily/core';
+import { createForm } from '@formily/core';
+import { createSchemaField } from '@formily/react';
+import React, { useEffect, useState } from 'react';
+import * as ICONS from '@ant-design/icons';
+import { Form, FormItem, Input, Password, Select, Switch } from '@formily/antd';
+import type { ISchema } from '@formily/json-schema';
+import { PlusOutlined } from '@ant-design/icons';
+import { action } from '@formily/reactive';
+import type { Response } from '@/utils/typings';
+import { service } from '@/pages/system/User';
+
+interface Props {
+  model: 'add' | 'edit' | 'query';
+  data: Partial<UserItem>;
+  close: () => void;
+}
+
+const Save = (props: Props) => {
+  const { model } = props;
+  const intl = useIntl();
+
+  const [data, setData] = useState<Partial<UserItem>>(props.data);
+
+  const getRole = () => service.queryRoleList();
+
+  const getOrg = () => service.queryOrgList();
+
+  const useAsyncDataSource = (api: any) => (field: Field) => {
+    field.loading = true;
+    api(field).then(
+      action.bound!((resp: Response<any>) => {
+        field.dataSource = resp.result?.map((item: Record<string, unknown>) => ({
+          label: item.name,
+          value: item.id,
+        }));
+        field.loading = false;
+      }),
+    );
+  };
+
+  const getUser = async () => {
+    if (props.data.id) {
+      console.log('id');
+      const response: Response<UserItem> = await service.queryDetail(props.data?.id);
+      if (response.status === 200) {
+        const temp = response.result as UserItem;
+        temp.orgIdList = (temp.orgList as { id: string; name: string }[]).map((item) => item.id);
+        temp.roleIdList = (temp.roleList as { id: string; name: string }[]).map((item) => item.id);
+        setData(temp);
+      }
+    }
+  };
+  useEffect(() => {
+    if (model === 'edit') {
+      getUser();
+    } else {
+      setData({});
+    }
+  }, [props.data, props.model]);
+
+  const form = createForm({
+    validateFirst: true,
+    initialValues: data,
+  });
+
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      Input,
+      Password,
+      Switch,
+      Select,
+    },
+    scope: {
+      icon(name: any) {
+        return React.createElement(ICONS[name]);
+      },
+    },
+  });
+
+  const schema: ISchema = {
+    type: 'object',
+    properties: {
+      name: {
+        title: intl.formatMessage({
+          id: 'pages.system.name',
+          defaultMessage: '姓名',
+        }),
+        type: 'string',
+        'x-decorator': 'FormItem',
+        'x-component': 'Input',
+        'x-component-props': {},
+        'x-decorator-props': {},
+        name: 'name',
+        required: true,
+      },
+      username: {
+        title: intl.formatMessage({
+          id: 'pages.system.username',
+          defaultMessage: '用户名',
+        }),
+        type: 'string',
+        'x-decorator': 'FormItem',
+        'x-component': 'Input',
+        'x-component-props': {
+          disabled: model === 'edit',
+        },
+        'x-decorator-props': {},
+        name: 'username',
+        required: true,
+      },
+      password: {
+        type: 'string',
+        title: intl.formatMessage({
+          id: 'pages.system.password',
+          defaultMessage: '密码',
+        }),
+        'x-decorator': 'FormItem',
+        'x-component': 'Password',
+        'x-component-props': {
+          checkStrength: true,
+          placeholder: '********',
+        },
+        required: model === 'add',
+        'x-reactions': [
+          {
+            dependencies: ['.confirmPassword'],
+            fulfill: {
+              state: {
+                selfErrors:
+                  '{{$deps[0] && $self.value && $self.value !==$deps[0] ? "确认密码不匹配" : ""}}',
+              },
+            },
+          },
+        ],
+        'x-decorator-props': {},
+        name: 'password',
+      },
+      confirmPassword: {
+        type: 'string',
+        title: intl.formatMessage({
+          id: 'pages.system.confirmPassword',
+          defaultMessage: '确认密码?',
+        }),
+        'x-decorator': 'FormItem',
+        'x-component': 'Password',
+        'x-component-props': {
+          checkStrength: true,
+          placeholder: '********',
+        },
+        'x-reactions': [
+          {
+            dependencies: ['.password'],
+            fulfill: {
+              state: {
+                selfErrors:
+                  '{{$deps[0] && $self.value && $self.value !== $deps[0] ? "确认密码不匹配" : ""}}',
+              },
+            },
+          },
+        ],
+        'x-decorator-props': {},
+        name: 'confirmPassword',
+        required: model === 'add',
+      },
+      roleIdList: {
+        title: '角色',
+        'x-decorator': 'FormItem',
+        'x-component': 'Select',
+        'x-component-props': {
+          mode: 'multiple',
+        },
+        'x-reactions': ['{{useAsyncDataSource(getRole)}}'],
+        'x-decorator-props': {
+          addonAfter: (
+            <a
+              // href={`${origin}/#/system/role`}
+              // target='_blank'
+              // rel='noreferrer'
+              onClick={() => {
+                // const test = window.open(`${origin}/#/system/role`);
+                // test!.onSuccess1 = (data: any) => {
+                //   form.setFieldState('role', state => {
+                //     state.dataSource = [...testEnum, { label: '测试数据A', value: 'testA' }];
+                //   });
+                // console.log(JSON.stringify(data));
+                // testEnum.push({label:'测试数据A',value:'testA'});
+                // setTestEnum([...testEnum, { label: '测试数据A', value: 'testA' }]);
+                // };
+              }}
+            >
+              <PlusOutlined />
+            </a>
+          ),
+        },
+      },
+      orgIdList: {
+        title: '部门',
+        'x-decorator': 'FormItem',
+        'x-component': 'Select',
+        'x-component-props': {
+          mode: 'multiple',
+          showArrow: true,
+        },
+        'x-decorator-props': {
+          addonAfter: (
+            <a>
+              <PlusOutlined />
+            </a>
+          ),
+        },
+        'x-reactions': ['{{useAsyncDataSource(getOrg)}}'],
+      },
+    },
+  };
+
+  const save = async () => {
+    const value = await form.submit<UserItem>();
+    const temp: any = {};
+    temp.id = value.id;
+    temp.user = value;
+    temp.orgIdList = value.orgIdList;
+    temp.roleIdList = value.roleIdList;
+    const response = await service.saveUser(temp, model);
+    if (response.status === 200) {
+      message.success(
+        intl.formatMessage({
+          id: 'pages.data.option.success',
+          defaultMessage: '操作成功',
+        }),
+      );
+    } else {
+      message.error('操作失败!');
+    }
+    props.close();
+  };
+
+  return (
+    <Modal
+      title={intl.formatMessage({
+        id: `pages.data.option.${model}`,
+        defaultMessage: '编辑',
+      })}
+      maskClosable={false}
+      visible={model !== 'query'}
+      onCancel={props.close}
+      onOk={save}
+    >
+      <Form form={form} labelCol={4} wrapperCol={18}>
+        <SchemaField schema={schema} scope={{ useAsyncDataSource, getRole, getOrg }} />
+      </Form>
+    </Modal>
+  );
+};
+export default Save;

+ 52 - 151
src/pages/system/User/index.tsx

@@ -1,36 +1,42 @@
+import Service from '@/pages/system/User/serivce';
 import { PageContainer } from '@ant-design/pro-layout';
-import { useEffect, useRef, useState } from 'react';
+import SearchComponent from '@/components/SearchComponent';
+import type { ActionType, ProColumns } from '@jetlinks/pro-table';
+import ProTable from '@jetlinks/pro-table';
+import { Button, Card, message, Popconfirm, Tooltip } from 'antd';
 import {
+  CloseCircleOutlined,
   EditOutlined,
   KeyOutlined,
-  CloseCircleOutlined,
   PlayCircleOutlined,
+  PlusOutlined,
 } from '@ant-design/icons';
-import { Tooltip, Popconfirm, message, Drawer, Card, Divider } from 'antd';
-import type { ProColumns, ActionType } from '@jetlinks/pro-table';
-import BaseCrud from '@/components/BaseCrud';
-import { CurdModel } from '@/components/BaseCrud/model';
-import BaseService from '@/utils/BaseService';
-import { observer } from '@formily/react';
-import { Store } from 'jetlinks-store';
-import SystemConst from '@/utils/const';
-import { useIntl } from '@@/plugin-locale/localeExports';
-import type { ISchema } from '@formily/json-schema';
-import Authorization from '@/components/Authorization';
 import autzModel from '@/components/Authorization/autz';
-import SearchComponent from '@/components/SearchComponent';
+import { useIntl } from '@@/plugin-locale/localeExports';
+import { useRef, useState } from 'react';
+import Save from './Save';
+import { observer } from '@formily/react';
+
+export const service = new Service('user');
 
-export const service = new BaseService<UserItem>('user');
 const User = observer(() => {
   const intl = useIntl();
   const actionRef = useRef<ActionType>();
 
-  const [model1, setModel] = useState(CurdModel.model);
-
-  useEffect(() => {
-    const modelSubscription = Store.subscribe(SystemConst.BASE_CURD_MODEL, setModel);
-    return () => modelSubscription.unsubscribe();
-  }, [CurdModel.model]);
+  const [model, setMode] = useState<'add' | 'edit' | 'query'>('query');
+  const [current, setCurrent] = useState<Partial<UserItem>>({});
+  const edit = async (record: UserItem) => {
+    setMode('edit');
+    setCurrent(record);
+    // const response: Response<UserItem> = await service.queryDetail(record.id);
+    // if (response.status === 200) {
+    //   const temp = response.result as UserItem;
+    //   temp.orgIdList = (temp.orgList as { id: string, name: string }[]).map((item) => item.id);
+    //   temp.roleIdList = (temp.roleList as { id: string, name: string }[]).map(item => item.id);
+    //   state.model = 'edit';
+    //   state.current = temp;
+    // }
+  };
 
   const columns: ProColumns<UserItem>[] = [
     {
@@ -133,14 +139,7 @@ const User = observer(() => {
       align: 'center',
       width: 200,
       render: (text, record) => [
-        <a
-          key="editable"
-          onClick={() => {
-            CurdModel.update(record);
-            CurdModel.model = 'edit';
-            setModel('edit');
-          }}
-        >
+        <a key="editable" onClick={() => edit(record)}>
           <Tooltip
             title={intl.formatMessage({
               id: 'pages.data.option.edit',
@@ -201,137 +200,39 @@ const User = observer(() => {
     },
   ];
 
-  const schema: ISchema = {
-    type: 'object',
-    properties: {
-      username: {
-        title: intl.formatMessage({
-          id: 'pages.system.username',
-          defaultMessage: '用户名',
-        }),
-        type: 'string',
-        'x-decorator': 'FormItem',
-        'x-component': 'Input',
-        'x-component-props': {
-          disabled: model1 === 'edit',
-        },
-        'x-decorator-props': {},
-        name: 'username',
-        required: true,
-      },
-      name: {
-        title: intl.formatMessage({
-          id: 'pages.system.name',
-          defaultMessage: '姓名',
-        }),
-        type: 'string',
-        'x-decorator': 'FormItem',
-        'x-component': 'Input',
-        'x-component-props': {},
-        'x-decorator-props': {},
-        name: 'name',
-        required: true,
-      },
-      password: {
-        type: 'string',
-        title: intl.formatMessage({
-          id: 'pages.system.password',
-          defaultMessage: '密码',
-        }),
-        'x-decorator': 'FormItem',
-        'x-component': 'Password',
-        'x-component-props': {
-          checkStrength: true,
-        },
-        // 'x-hidden': model === 'edit',
-        'x-reactions': [
-          {
-            dependencies: ['.confirmPassword'],
-            fulfill: {
-              state: {
-                errors:
-                  '{{$deps[0] && $self.value && $self.value !==$deps[0] ? "确认密码不匹配" : ""}}',
-              },
-            },
-          },
-        ],
-        'x-decorator-props': {},
-        name: 'password',
-        required: false,
-      },
-      confirmPassword: {
-        type: 'string',
-        title: intl.formatMessage({
-          id: 'pages.system.confirmPassword',
-          defaultMessage: '确认密码?',
-        }),
-        'x-decorator': 'FormItem',
-        'x-component': 'Password',
-        // 'x-hidden': model === 'edit',
-        'x-component-props': {
-          checkStrength: true,
-        },
-        'x-reactions': [
-          {
-            dependencies: ['.password'],
-            fulfill: {
-              state: {
-                errors:
-                  '{{$deps[0] && $self.value && $self.value !== $deps[0] ? "确认密码不匹配" : ""}}',
-              },
-            },
-          },
-        ],
-        'x-decorator-props': {},
-        name: 'confirmPassword',
-        required: false,
-      },
-    },
-  };
-
+  const [param, setParam] = useState({});
   return (
     <PageContainer>
-      <Card>
-        <SearchComponent<UserItem>
+      <Card style={{ marginBottom: '20px' }}>
+        <SearchComponent
           field={columns}
-          onSearch={async (data) => {
-            message.success(JSON.stringify(data));
-          }}
-          target="user-search"
+          onSearch={(data) => setParam({ terms: data })}
+          target="user"
         />
       </Card>
-      <Divider />
-      <BaseCrud<UserItem>
-        actionRef={actionRef}
+      <ProTable<UserItem>
+        params={param}
         columns={columns}
         search={false}
-        service={service}
-        title={intl.formatMessage({
-          id: 'pages.system.user',
-          defaultMessage: '用户管理',
-        })}
-        schema={schema}
+        request={async (params = {}) => service.query(params)}
+        toolBarRender={() => [
+          <Button
+            onClick={() => {
+              setMode('add');
+            }}
+            key="button"
+            icon={<PlusOutlined />}
+            type="primary"
+          >
+            {intl.formatMessage({
+              id: 'pages.data.option.add',
+              defaultMessage: '新增',
+            })}
+          </Button>,
+        ]}
       />
-      <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}
-        />
-      </Drawer>
+      <Save model={model} close={() => setMode('query')} data={current} />
     </PageContainer>
   );
 });
-
 export default User;

+ 41 - 0
src/pages/system/User/serivce.ts

@@ -0,0 +1,41 @@
+import BaseService from '@/utils/BaseService';
+import { request } from 'umi';
+import SystemConst from '@/utils/const';
+
+class Service extends BaseService<UserItem> {
+  queryRoleList = (params?: any) =>
+    request(`${SystemConst.API_BASE}/role/_query/no-paging?paging=false`, {
+      method: 'GET',
+      params,
+    });
+
+  queryOrgList = (params?: any) =>
+    request(`${SystemConst.API_BASE}/organization/_all/tree`, {
+      method: 'GET',
+      params,
+    });
+
+  queryDetail = (id: string) =>
+    request(`/${SystemConst.API_BASE}/user/detail/${id}`, {
+      method: 'GET',
+    });
+
+  saveUser = (data: UserItem, type: 'add' | 'edit' | 'query') => {
+    const map = {
+      add: {
+        api: '_create',
+        method: 'POST',
+      },
+      edit: {
+        api: `${data.id}/_update`,
+        method: 'PUT',
+      },
+    };
+    return request(`/${SystemConst.API_BASE}/user/detail/${map[type].api}`, {
+      method: map[type].method,
+      data,
+    });
+  };
+}
+
+export default Service;

+ 5 - 0
src/pages/system/User/typings.d.ts

@@ -8,4 +8,9 @@ type UserItem = {
   telephone?: string;
   avatar?: string;
   description?: string;
+
+  orgList?: { id: string; name: string }[] | string[];
+  roleList?: { id: string; name: string }[] | string[];
+  orgIdList?: string[];
+  roleIdList?: string[];
 };

+ 2 - 2
src/utils/BaseService.ts

@@ -19,8 +19,8 @@ class BaseService<T> implements IBaseService<T> {
     this.uri = `/${SystemConst.API_BASE}/${uri}`;
   }
 
-  query(params: any): Promise<any> {
-    return request(`${this.uri}/_query/`, { params, method: 'GET' });
+  query(data: any): Promise<any> {
+    return request(`${this.uri}/_query/`, { data, method: 'POST' });
   }
 
   queryNoPaging(params: any): Promise<unknown> {

+ 27 - 0
src/utils/menu.ts

@@ -0,0 +1,27 @@
+// 路由components映射
+// const findComponents = (files: __WebpackModuleApi.RequireContext) => {
+//   const modules = {};
+//   files.keys().forEach((key) => {
+//     // 删除路径开头的./ 以及结尾的 /index;
+//     const str = key.replace(/(\.\/|\.tsx)/g, '').replace('/index', '');
+//     modules[str] = files(key).default;
+//   });
+//   return modules;
+// };
+//
+// /**
+//  * 处理为正确的路由格式
+//  * @param extraRoutes 后端菜单数据
+//  */
+// const getRoutes = (extraRoutes: any[]) => {
+//   const allComponents = findComponents(require.context('@/pages', true, /index(\.tsx)$/));
+//   return extraRoutes.map((route) => {
+//     const component = allComponents[route.key];
+//     return {
+//       ...route,
+//       component,
+//     };
+//   });
+// };
+//
+// export default getRoutes;