sun-chaochao 3 лет назад
Родитель
Сommit
32fa58e9db

BIN
public/images/channel/1.png


BIN
public/images/channel/2.png


BIN
public/images/channel/3.png


BIN
public/images/channel/4.png


BIN
public/images/channel/background.png


+ 2 - 2
src/app.tsx

@@ -13,7 +13,7 @@ import type { RequestOptionsInit } from 'umi-request';
 import ReconnectingWebSocket from 'reconnecting-websocket';
 import SystemConst from '@/utils/const';
 import { service as MenuService } from '@/pages/system/Menu';
-import getRoutes, { getMenus, handleRoutes, saveMenusCache } from '@/utils/menu';
+import getRoutes, { extraRouteArr, getMenus, handleRoutes, saveMenusCache } from '@/utils/menu';
 import { AIcon } from '@/components';
 
 const isDev = process.env.NODE_ENV === 'development';
@@ -264,7 +264,7 @@ export function render(oldRender: any) {
             url: '/demo',
           });
         }
-        extraRoutes = handleRoutes(res.result);
+        extraRoutes = handleRoutes([...extraRouteArr, ...res.result]);
         saveMenusCache(extraRoutes);
       }
       oldRender();

+ 3 - 1
src/components/RightContent/AvatarDropdown.tsx

@@ -8,6 +8,7 @@ import styles from './index.less';
 import type { MenuInfo } from 'rc-menu/lib/interface';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import Service from '@/pages/user/Login/service';
+import { getMenuPathByCode } from '@/utils/menu';
 
 export type GlobalHeaderRightProps = {
   menu?: boolean;
@@ -47,7 +48,8 @@ const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => {
         await loginOut();
         return;
       }
-      history.push(`/account/${key}`);
+      // console.log(key)
+      history.push(getMenuPathByCode('account/Center'));
     },
     [initialState, setInitialState],
   );

+ 76 - 0
src/pages/account/Center/bind/index.tsx

@@ -0,0 +1,76 @@
+import { Button, Card, Col, message, Row } from 'antd';
+import { useEffect, useState } from 'react';
+import Service from '@/pages/account/Center/service';
+
+export const service = new Service();
+
+const Bind = () => {
+  const [bindUser, setBindUser] = useState<any>();
+  const [user, setUser] = useState<any>();
+  const [code, setCode] = useState<string>('');
+
+  const bindUserInfo = (params: string) => {
+    service.bindUserInfo(params).then((res) => {
+      if (res.status === 200) {
+        setBindUser(res.result);
+      }
+    });
+  };
+  const getDetail = () => {
+    service.getUserDetail().subscribe((res) => {
+      setUser(res.result);
+    });
+  };
+
+  useEffect(() => {
+    // window.open('http://z.jetlinks.cn')
+    // const item = `http://pro.jetlinks.cn/#/user/login?sso=true&code=4decc08bcb87f3a4fbd74976fd86cd3d&redirect=http://pro.jetlinks.cn/jetlinks`;
+    const params = window.location.href.split('?')[1].split('&')[1].split('=')[1];
+    setCode(params);
+    bindUserInfo(params);
+    getDetail();
+  }, []);
+  return (
+    <>
+      <Card>
+        <Row gutter={[24, 24]}>
+          <Col span={12}>
+            <Card title="个人信息">
+              <p>登录账号:{user?.name}</p>
+              <p>姓名:{user?.name}</p>
+            </Card>
+          </Col>
+          <Col span={12}>
+            <Card title="三方账号信息">
+              <p>类型:{bindUser?.type}</p>
+              <p>组织:{bindUser?.providerName}</p>
+            </Card>
+          </Col>
+        </Row>
+        <Row gutter={[24, 24]}>
+          <Col span={24} style={{ textAlign: 'center', marginTop: 20 }}>
+            <Button
+              type="primary"
+              onClick={() => {
+                service.bind(code).then((res) => {
+                  if (res.status === 200) {
+                    message.success('绑定成功');
+                    if ((window as any).onBindSuccess) {
+                      (window as any).onBindSuccess(res);
+                      setTimeout(() => window.close(), 300);
+                    }
+                  } else {
+                    message.error('绑定失败');
+                  }
+                });
+              }}
+            >
+              立即绑定
+            </Button>
+          </Col>
+        </Row>
+      </Card>
+    </>
+  );
+};
+export default Bind;

+ 110 - 0
src/pages/account/Center/edit/infoEdit.tsx

@@ -0,0 +1,110 @@
+import { Modal, Form, Input, Col, Row } from 'antd';
+
+interface Props {
+  data: any;
+  save: Function;
+  close: Function;
+}
+
+const InfoEdit = (props: Props) => {
+  const [form] = Form.useForm();
+  const { data } = props;
+  const handleSave = async () => {
+    const formData = await form.validateFields();
+    console.log(formData);
+    props.save({
+      name: formData.name,
+      email: formData.email || '',
+      telephone: formData.telephone || '',
+    });
+  };
+
+  return (
+    <Modal
+      title="编辑"
+      visible
+      width="40vw"
+      destroyOnClose
+      onOk={handleSave}
+      onCancel={() => {
+        props.close();
+      }}
+    >
+      <Form
+        form={form}
+        layout="vertical"
+        initialValues={{
+          name: data.name,
+          username: data.username,
+          role: data?.roleList[0]?.name,
+          org: data?.orgList[0]?.name,
+          telephone: data.telephone,
+          email: data.email,
+        }}
+      >
+        <Row gutter={[24, 24]}>
+          <Col span={12}>
+            <Form.Item
+              label="姓名"
+              required
+              name="name"
+              rules={[
+                { type: 'string', max: 64 },
+                { required: true, message: '姓名必填' },
+              ]}
+            >
+              <Input placeholder="请输入姓名" />
+            </Form.Item>
+          </Col>
+          <Col span={12}>
+            <Form.Item label="用户名" name="username">
+              <Input placeholder="请输入用户名" disabled />
+            </Form.Item>
+          </Col>
+        </Row>
+        <Row gutter={[24, 24]}>
+          <Col span={12}>
+            <Form.Item label="角色" name="role">
+              <Input placeholder="请输入姓名" disabled />
+            </Form.Item>
+          </Col>
+          <Col span={12}>
+            <Form.Item label="部门" name="org">
+              <Input placeholder="请输入用户名" disabled />
+            </Form.Item>
+          </Col>
+        </Row>
+        <Row gutter={[24, 24]}>
+          <Col span={12}>
+            <Form.Item
+              label="手机号"
+              name="telephone"
+              rules={[
+                {
+                  pattern: /^1[3456789]\d{9}$/,
+                  message: '请输入正确手机号',
+                },
+              ]}
+            >
+              <Input placeholder="请输入手机号" />
+            </Form.Item>
+          </Col>
+          <Col span={12}>
+            <Form.Item
+              label="邮箱"
+              name="email"
+              rules={[
+                {
+                  type: 'email',
+                },
+              ]}
+            >
+              <Input placeholder="请输入邮箱" />
+            </Form.Item>
+          </Col>
+        </Row>
+      </Form>
+    </Modal>
+  );
+};
+export default InfoEdit;

+ 234 - 0
src/pages/account/Center/edit/passwordEdit.tsx

@@ -0,0 +1,234 @@
+import { Modal } from 'antd';
+import { createSchemaField } from '@formily/react';
+import { Form, FormItem, Password, Input } from '@formily/antd';
+import { ISchema } from '@formily/json-schema';
+import { useIntl } from 'umi';
+import { useMemo } from 'react';
+import { createForm } from '@formily/core';
+import { service } from '@/pages/account/Center';
+
+interface Props {
+  visible: boolean;
+  close: Function;
+  save: Function;
+}
+
+const PasswordEdit = (props: Props) => {
+  const intl = useIntl();
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      Password,
+      Input,
+    },
+  });
+
+  const schema: ISchema = {
+    type: 'object',
+    properties: {
+      oldPassword: {
+        type: 'string',
+        title: '旧密码',
+        'x-decorator': 'FormItem',
+        'x-component': 'Input',
+        'x-component-props': {
+          checkStrength: true,
+          placeholder: '请输入旧密码',
+        },
+        required: true,
+        'x-validator': [
+          // {
+          //   max: 128,
+          //   message: '密码最多可输入128位',
+          // },
+          // {
+          //   min: 8,
+          //   message: '密码不能少于8位',
+          // },
+          {
+            required: true,
+            message: '请输入密码',
+          },
+          {
+            triggerType: 'onBlur',
+            validator: (value: string) => {
+              return new Promise((resolve) => {
+                service
+                  .validatePassword(value)
+                  .then((resp) => {
+                    if (resp.status === 200) {
+                      if (resp.result.passed) {
+                        resolve('');
+                      } else {
+                        resolve(resp.result.reason);
+                      }
+                    }
+                    resolve('');
+                  })
+                  .catch(() => {
+                    return '验证失败!';
+                  });
+              });
+            },
+          },
+        ],
+      },
+      newPassword: {
+        type: 'string',
+        title: intl.formatMessage({
+          id: 'pages.system.password',
+          defaultMessage: '密码',
+        }),
+        'x-decorator': 'FormItem',
+        'x-component': 'Password',
+        'x-component-props': {
+          checkStrength: true,
+          placeholder: '请输入密码',
+        },
+        required: true,
+
+        'x-reactions': [
+          {
+            dependencies: ['.confirmPassword'],
+            fulfill: {
+              state: {
+                selfErrors:
+                  '{{$deps[0] && $self.value && $self.value !==$deps[0] ? "两次密码输入不一致" : ""}}',
+              },
+            },
+          },
+        ],
+        name: 'password',
+        'x-validator': [
+          // {
+          //   max: 128,
+          //   message: '密码最多可输入128位',
+          // },
+          // {
+          //   min: 8,
+          //   message: '密码不能少于8位',
+          // },
+          {
+            required: true,
+            message: '请输入密码',
+          },
+          {
+            triggerType: 'onBlur',
+            validator: (value: string) => {
+              return new Promise((resolve) => {
+                service
+                  .validateField('password', value)
+                  .then((resp) => {
+                    if (resp.status === 200) {
+                      if (resp.result.passed) {
+                        resolve('');
+                      } else {
+                        resolve(resp.result.reason);
+                      }
+                    }
+                    resolve('');
+                  })
+                  .catch(() => {
+                    return '验证失败!';
+                  });
+              });
+            },
+          },
+        ],
+      },
+      confirmPassword: {
+        type: 'string',
+        title: intl.formatMessage({
+          id: 'pages.system.confirmPassword',
+          defaultMessage: '确认密码?',
+        }),
+        'x-decorator': 'FormItem',
+        'x-component': 'Password',
+        'x-component-props': {
+          checkStrength: true,
+          placeholder: '请再次输入密码',
+        },
+        'x-validator': [
+          // {
+          //   max: 128,
+          //   message: '密码最多可输入128位',
+          // },
+          // {
+          //   min: 8,
+          //   message: '密码不能少于8位',
+          // },
+          {
+            required: true,
+            message: '请输入确认密码',
+          },
+          {
+            triggerType: 'onBlur',
+            validator: (value: string) => {
+              return new Promise((resolve) => {
+                service
+                  .validateField('password', value)
+                  .then((resp) => {
+                    if (resp.status === 200) {
+                      if (resp.result.passed) {
+                        resolve('');
+                      } else {
+                        resolve(resp.result.reason);
+                      }
+                    }
+                    resolve('');
+                  })
+                  .catch(() => {
+                    return '验证失败!';
+                  });
+              });
+            },
+          },
+        ],
+        'x-reactions': [
+          {
+            dependencies: ['.password'],
+            fulfill: {
+              state: {
+                selfErrors:
+                  '{{$deps[0] && $self.value && $self.value !== $deps[0] ? "两次密码输入不一致" : ""}}',
+              },
+            },
+          },
+        ],
+        'x-decorator-props': {},
+        name: 'confirmPassword',
+      },
+    },
+  };
+
+  const form = useMemo(() => createForm({}), [props.visible]);
+  return (
+    <Modal
+      title="重置密码"
+      visible
+      onCancel={() => props.close()}
+      onOk={async () => {
+        const value: { oldPassword: string; newPassword: string; confirmPassword: string } =
+          await form.submit();
+        console.log(value);
+        props.save({
+          oldPassword: value.oldPassword,
+          newPassword: value.newPassword,
+        });
+        // if (props.data.id) {
+        //   const resp = await service.resetPassword(props.data.id, value.confirmPassword);
+        //   if (resp.status === 200) {
+        //     message.success('操作成功');
+        //     props.close();
+        //   }
+        // }
+        // props.close();
+      }}
+    >
+      <Form form={form} layout="vertical">
+        <SchemaField schema={schema} />
+      </Form>
+    </Modal>
+  );
+};
+export default PasswordEdit;

+ 29 - 0
src/pages/account/Center/index.less

@@ -0,0 +1,29 @@
+.info {
+  margin-top: 15px;
+}
+.top {
+  display: flex;
+  width: 100%;
+  .avatar {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: space-between;
+    width: 350px;
+    height: 200px;
+  }
+  .content {
+    width: 80%;
+    padding-top: 15px;
+  }
+  .action {
+    position: relative;
+    top: 15px;
+    right: 20px;
+  }
+}
+.bind {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}

+ 294 - 0
src/pages/account/Center/index.tsx

@@ -0,0 +1,294 @@
+import { PageContainer } from '@ant-design/pro-layout';
+import { Card, Upload, message, Avatar, Button, Descriptions, Divider, Col, Row } from 'antd';
+import { useEffect, useState } from 'react';
+import styles from './index.less';
+import { UploadProps } from 'antd/lib/upload';
+import Token from '@/utils/token';
+import SystemConst from '@/utils/const';
+import { EditOutlined, LockOutlined, UploadOutlined, UserOutlined } from '@ant-design/icons';
+import InfoEdit from './edit/infoEdit';
+import PasswordEdit from './edit/passwordEdit';
+import Service from '@/pages/account/Center/service';
+import moment from 'moment';
+
+export const service = new Service();
+
+const Center = () => {
+  const [data, setData] = useState<any>();
+  const [imageUrl, setImageUrl] = useState<string>('');
+  // const [loading, setLoading] = useState<boolean>(false)
+  const [infos, setInfos] = useState<boolean>(false);
+  const [password, setPassword] = useState<boolean>(false);
+  const [bindList, setBindList] = useState<any>([]);
+
+  const iconMap = new Map();
+  iconMap.set('dingtalk', require('/public/images/notice/dingtalk.png'));
+  iconMap.set('wechat-webapp', require('/public/images/notice/wechat.png'));
+
+  const bGroundMap = new Map();
+  bGroundMap.set('dingtalk', require('/public/images/notice/dingtalk-background.png'));
+  bGroundMap.set('wechat-webapp', require('/public/images/notice/wechat-background.png'));
+
+  const uploadProps: UploadProps = {
+    showUploadList: false,
+    action: `/${SystemConst.API_BASE}/file/static`,
+    headers: {
+      'X-Access-Token': Token.get(),
+    },
+    beforeUpload(file) {
+      const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
+      if (!isJpgOrPng) {
+        message.error('请上传.png.jpg格式的文件');
+      }
+      return isJpgOrPng;
+    },
+    onChange(info) {
+      if (info.file.status === 'uploading') {
+        // setLoading(true);
+      }
+      if (info.file.status === 'done') {
+        setImageUrl(info.file.response.result);
+        service
+          .saveUserDetail({
+            name: data.name,
+            avatar: info.file.response.result,
+          })
+          .subscribe((res) => {
+            if (res.status === 200) {
+              setImageUrl(info.file.response.result);
+              message.success('上传成功');
+            }
+          });
+        // setLoading(false);
+      }
+    },
+  };
+
+  const getDetail = () => {
+    service.getUserDetail().subscribe((res) => {
+      setData(res.result);
+      setImageUrl(res.result.avatar);
+    });
+  };
+  const saveInfo = (parms: UserDetail) => {
+    service.saveUserDetail(parms).subscribe((res) => {
+      if (res.status === 200) {
+        message.success('保存成功');
+        getDetail();
+        setInfos(false);
+      } else {
+        message.success('保存失败');
+      }
+    });
+  };
+  const savePassword = (parms: { oldPassword: string; newPassword: string }) => {
+    service.savePassWord(parms).subscribe((res) => {
+      if (res.status === 200) {
+        message.success('保存成功');
+      }
+    });
+  };
+  const getBindInfo = () => {
+    service.bindInfo().then((res) => {
+      if (res.status === 200) {
+        console.log(res);
+        setBindList(res.result);
+      }
+    });
+  };
+  const unBind = (type: string, provider: string) => {
+    service.unbind(type, provider).then((res) => {
+      if (res.status === 200) {
+        message.success('解绑成功');
+        getBindInfo();
+      }
+    });
+  };
+
+  useEffect(() => {
+    getDetail();
+    getBindInfo();
+  }, []);
+
+  return (
+    <PageContainer>
+      <Card>
+        <div className={styles.top}>
+          <div className={styles.avatar}>
+            <div>
+              {data?.avatar ? (
+                <Avatar size={140} src={imageUrl} />
+              ) : (
+                <Avatar size={140} icon={<UserOutlined />} />
+              )}
+            </div>
+            <Upload {...uploadProps}>
+              <Button>
+                <UploadOutlined />
+                更换头像
+              </Button>
+            </Upload>
+          </div>
+          <div className={styles.content}>
+            <Descriptions column={4} layout="vertical">
+              <Descriptions.Item label="登录账号">{data?.username}</Descriptions.Item>
+              <Descriptions.Item label="账号ID">{data?.id}</Descriptions.Item>
+              <Descriptions.Item label="注册时间">
+                {moment(data?.createTime).format('YYYY-MM-DD HH:mm:ss')}
+              </Descriptions.Item>
+              <Descriptions.Item label="电话">{data?.telephone || '-'}</Descriptions.Item>
+              <Descriptions.Item label="姓名">{data?.name}</Descriptions.Item>
+              <Descriptions.Item label="角色">{data?.roleList[0]?.name || '-'}</Descriptions.Item>
+              <Descriptions.Item label="部门">{data?.orgList[0]?.name || '-'}</Descriptions.Item>
+              <Descriptions.Item label="邮箱">{data?.email || '-'}</Descriptions.Item>
+            </Descriptions>
+          </div>
+          <a>
+            {' '}
+            <EditOutlined
+              className={styles.action}
+              onClick={() => {
+                setInfos(true);
+              }}
+            />
+          </a>
+        </div>
+      </Card>
+      <Card
+        className={styles.info}
+        title={
+          <div style={{ fontSize: '22px' }}>
+            <Divider type="vertical" style={{ backgroundColor: '#2F54EB', width: 3 }} />
+            修改密码
+          </div>
+        }
+        extra={
+          <a>
+            {' '}
+            <EditOutlined
+              onClick={() => {
+                setPassword(true);
+              }}
+            />
+          </a>
+        }
+      >
+        <div style={{ display: 'flex', alignItems: 'flex-end' }}>
+          <div>
+            <LockOutlined
+              style={{
+                color: '#1d39c4',
+                fontSize: '70px',
+              }}
+            />
+          </div>
+          <div style={{ marginLeft: 5, color: 'rgba(0, 0, 0, 0.55)' }}>
+            安全性高的密码可以使帐号更安全。建议您定期更换密码,设置一个包含字母,符号或数字中至少两项且长度超过8位的密码
+          </div>
+        </div>
+      </Card>
+      <Card
+        className={styles.info}
+        title={
+          <div style={{ fontSize: '22px' }}>
+            <Divider type="vertical" style={{ backgroundColor: '#2F54EB', width: 3 }} />
+            绑定三方账号
+          </div>
+        }
+      >
+        <Row gutter={[24, 24]}>
+          {bindList.map((item: any) => (
+            <Col key={item.type}>
+              <Card
+                style={{
+                  background: `url(${bGroundMap.get(item.type)}) no-repeat`,
+                  backgroundSize: '100% 100%',
+                  width: 415,
+                }}
+              >
+                <div className={styles.bind}>
+                  <div>
+                    <img style={{ height: 56 }} src={iconMap.get(item.type)} />
+                  </div>
+                  <div>
+                    {item.bound ? (
+                      <div>
+                        <div style={{ fontSize: '22px' }}>绑定名:{item.others.name}</div>
+                        <div
+                          style={{
+                            fontSize: '14px',
+                            lineHeight: '20px',
+                            marginTop: '5px',
+                            color: '#00000073',
+                          }}
+                        >
+                          绑定时间: 2022-01-02 09:00:00
+                        </div>
+                      </div>
+                    ) : (
+                      <div style={{ fontSize: '22px' }}>{`${
+                        item.type === 'dingtalk' ? '钉钉' : '微信'
+                      }未绑定`}</div>
+                    )}
+                  </div>
+                  <div>
+                    {item.bound ? (
+                      <Button
+                        onClick={() => {
+                          unBind(item.type, item.provider);
+                        }}
+                      >
+                        解除绑定
+                      </Button>
+                    ) : (
+                      <Button
+                        type="primary"
+                        onClick={() => {
+                          const items: any = window.open(
+                            `/${SystemConst.API_BASE}/sso/${item.provider}/login`,
+                          );
+                          //  const items:any= window.open(`/#/account/Center/bind`);
+                          items!.onBindSuccess = (value: any) => {
+                            if (value.status === 200) {
+                              getBindInfo();
+                            }
+                          };
+                          // history.push(getMenuPathByCode('account/Center/bind'));
+                        }}
+                      >
+                        立即绑定
+                      </Button>
+                    )}
+                  </div>
+                </div>
+              </Card>
+            </Col>
+          ))}
+        </Row>
+      </Card>
+      {infos && (
+        <InfoEdit
+          data={data}
+          save={(item: any) => {
+            saveInfo(item);
+          }}
+          close={() => {
+            setInfos(false);
+          }}
+        />
+      )}
+      {password && (
+        <PasswordEdit
+          save={(item: any) => {
+            savePassword(item);
+          }}
+          visible={password}
+          close={() => {
+            setPassword(false);
+          }}
+        />
+      )}
+    </PageContainer>
+  );
+};
+export default Center;

+ 66 - 0
src/pages/account/Center/service.ts

@@ -0,0 +1,66 @@
+import BaseService from '@/utils/BaseService';
+import { request } from 'umi';
+import { defer, from } from 'rxjs';
+import { map } from 'rxjs/operators';
+import SystemConst from '@/utils/const';
+
+class Service extends BaseService<UserItem> {
+  getUserDetail = (params?: any) =>
+    defer(() =>
+      from(
+        request(`/${SystemConst.API_BASE}/user/detail`, {
+          method: 'GET',
+          params,
+        }),
+      ),
+    ).pipe(map((item) => item));
+
+  saveUserDetail = (data: any) =>
+    defer(() =>
+      from(
+        request(`/${SystemConst.API_BASE}/user/detail`, {
+          method: 'PUT',
+          data,
+        }),
+      ),
+    ).pipe(map((item) => item));
+  savePassWord = (data: any) =>
+    defer(() =>
+      from(
+        request(`/${SystemConst.API_BASE}/user/passwd`, {
+          method: 'PUT',
+          data,
+        }),
+      ),
+    ).pipe(map((item) => item));
+
+  validateField = (type: 'username' | 'password', name: string) =>
+    request(`/${SystemConst.API_BASE}/user/${type}/_validate`, {
+      method: 'POST',
+      data: name,
+    });
+  validatePassword = (password: string) =>
+    request(`/${SystemConst.API_BASE}/user/me/password/_validate`, {
+      method: 'POST',
+      data: password,
+    });
+  bindInfo = (params?: any) =>
+    request(`/${SystemConst.API_BASE}/sso/me/bindings`, {
+      method: 'GET',
+      params,
+    });
+  bindUserInfo = (code: string) =>
+    request(`/${SystemConst.API_BASE}/sso/bind-code/${code}`, {
+      method: 'GET',
+    });
+  bind = (code: string) =>
+    request(`/${SystemConst.API_BASE}/sso/me/bind/${code}`, {
+      method: 'POST',
+    });
+  unbind = (type: string, provider: string) =>
+    request(`/${SystemConst.API_BASE}/sso/me/${type}/${provider}/unbind`, {
+      method: 'POST',
+    });
+}
+
+export default Service;

+ 21 - 0
src/pages/account/Center/typings.d.ts

@@ -0,0 +1,21 @@
+type UserItem = {
+  id: string;
+  name: string;
+  status: number;
+  username: string;
+  createTime: number;
+  email?: string;
+  telephone?: string;
+  avatar?: string;
+  description?: string;
+  orgList?: { id: string; name: string }[] | string[];
+  roleList?: { id: string; name: string }[] | string[];
+  orgIdList?: string[];
+  roleIdList?: string[];
+};
+type UserDetail = {
+  name: string;
+  emmail?: string;
+  telephone?: string;
+  avatar?: string;
+};

+ 26 - 0
src/pages/link/Channel/Opcua/index.less

@@ -0,0 +1,26 @@
+.topCard {
+  display: flex;
+  align-items: center;
+  .img {
+    position: relative;
+    top: 10px;
+    left: 20px;
+  }
+  .text {
+    margin-left: 24px;
+    .p1 {
+      height: 22px;
+      margin-bottom: 7px;
+      font-weight: 700;
+      font-size: 18px;
+      line-height: 22px;
+    }
+    .p2 {
+      height: 20px;
+      color: rgba(0, 0, 0, 0.75);
+      font-weight: 400;
+      font-size: 12px;
+      line-height: 20px;
+    }
+  }
+}

+ 241 - 0
src/pages/link/Channel/Opcua/index.tsx

@@ -0,0 +1,241 @@
+import { PageContainer } from '@ant-design/pro-layout';
+import ProTable, { ActionType, ProColumns } from '@jetlinks/pro-table';
+import { Card, Col, Row, Badge } from 'antd';
+import styles from './index.less';
+import { PermissionButton } from '@/components';
+import {
+  DeleteOutlined,
+  EditOutlined,
+  LinkOutlined,
+  PlayCircleOutlined,
+  PlusOutlined,
+  StopOutlined,
+} from '@ant-design/icons';
+import { useIntl } from 'umi';
+import { useRef } from 'react';
+import SearchComponent from '@/components/SearchComponent';
+
+const Opcua = () => {
+  const intl = useIntl();
+  const actionRef = useRef<ActionType>();
+  // const [param, setParam] = useState({});
+  const { permission } = PermissionButton.usePermission('link/Channel/Opcua');
+
+  const iconMap = new Map();
+  iconMap.set('1', require('/public/images/channel/1.png'));
+  iconMap.set('2', require('/public/images/channel/2.png'));
+  iconMap.set('3', require('/public/images/channel/3.png'));
+  iconMap.set('4', require('/public/images/channel/4.png'));
+  const background = require('/public/images/channel/background.png');
+
+  const columns: ProColumns<any>[] = [
+    {
+      title: '通道名称',
+      dataIndex: 'name',
+    },
+    {
+      title: 'IP',
+      dataIndex: 'ip',
+    },
+    {
+      title: '端口',
+      dataIndex: 'local',
+    },
+    {
+      title: '状态',
+      dataIndex: 'state',
+      renderText: (state) => (
+        <Badge text={state?.text} status={state?.value === 'disabled' ? 'error' : 'success'} />
+      ),
+    },
+    {
+      title: '操作',
+      valueType: 'option',
+      align: 'center',
+      width: 200,
+      render: (text, record) => [
+        <PermissionButton
+          isPermission={permission.update}
+          key="edit"
+          onClick={() => {
+            // setVisible(true);
+            // setCurrent(record);
+          }}
+          type={'link'}
+          style={{ padding: 0 }}
+          tooltip={{
+            title: intl.formatMessage({
+              id: 'pages.data.option.edit',
+              defaultMessage: '编辑',
+            }),
+          }}
+        >
+          <EditOutlined />
+        </PermissionButton>,
+        <PermissionButton
+          type="link"
+          key={'action'}
+          style={{ padding: 0 }}
+          popConfirm={{
+            title: intl.formatMessage({
+              id: `pages.data.option.${
+                record.state.value !== 'notActive' ? 'disabled' : 'enabled'
+              }.tips`,
+              defaultMessage: '确认禁用?',
+            }),
+            onConfirm: async () => {
+              // if (record.state.value !== 'notActive') {
+              //   await service.undeployDevice(record.id);
+              // } else {
+              //   await service.deployDevice(record.id);
+              // }
+              // message.success(
+              //   intl.formatMessage({
+              //     id: 'pages.data.option.success',
+              //     defaultMessage: '操作成功!',
+              //   }),
+              // );
+              // actionRef.current?.reload();
+            },
+          }}
+          isPermission={permission.action}
+          tooltip={{
+            title: intl.formatMessage({
+              id: `pages.data.option.${
+                record.state.value !== 'notActive' ? 'disabled' : 'enabled'
+              }`,
+              defaultMessage: record.state.value !== 'notActive' ? '禁用' : '启用',
+            }),
+          }}
+        >
+          {record.state.value !== 'notActive' ? <StopOutlined /> : <PlayCircleOutlined />}
+        </PermissionButton>,
+        <PermissionButton
+          isPermission={permission.view}
+          style={{ padding: 0 }}
+          key="button"
+          type="link"
+          tooltip={{
+            title: '设备接入',
+          }}
+          onClick={() => {}}
+        >
+          <LinkOutlined />
+        </PermissionButton>,
+        <PermissionButton
+          isPermission={permission.delete}
+          style={{ padding: 0 }}
+          popConfirm={{
+            title: '确认删除',
+            onConfirm: async () => {
+              // const resp: any = await service.remove(record.id);
+              // if (resp.status === 200) {
+              //   message.success(
+              //     intl.formatMessage({
+              //       id: 'pages.data.option.success',
+              //       defaultMessage: '操作成功!',
+              //     }),
+              //   );
+              //   actionRef.current?.reload();
+              // }
+            },
+          }}
+          key="button"
+          type="link"
+        >
+          <DeleteOutlined />
+        </PermissionButton>,
+      ],
+    },
+  ];
+
+  const topCard = [
+    {
+      numeber: '1',
+      title: 'OPC UA通道',
+      text: '配置OPC UA通道',
+    },
+    {
+      numeber: '2',
+      title: '设备接入网关',
+      text: '创建OPC UA设备接入网关',
+    },
+    {
+      numeber: '3',
+      title: '创建产品',
+      text: '创建产品,并选择接入方式为OPC UA',
+    },
+    {
+      numeber: '4',
+      title: '添加设备',
+      text: '添加设备,单独为每一个设备进行数据点绑定',
+    },
+  ];
+  return (
+    <PageContainer>
+      <Card style={{ marginBottom: 10 }}>
+        <Row gutter={[24, 24]}>
+          {topCard.map((item) => (
+            <Col span={6}>
+              <Card>
+                <div className={styles.topCard}>
+                  <div
+                    style={{
+                      background: `url(${background}) no-repeat`,
+                      backgroundSize: '100% 100%',
+                      width: '56px',
+                      height: '56px',
+                    }}
+                  >
+                    <img src={iconMap.get(item.numeber)} className={styles.img} />
+                  </div>
+                  <div className={styles.text}>
+                    <p className={styles.p1}>{item.title}</p>
+                    <p className={styles.p2}>{item.text}</p>
+                  </div>
+                </div>
+              </Card>
+            </Col>
+          ))}
+        </Row>
+      </Card>
+
+      <SearchComponent<any>
+        field={columns}
+        target="opcua"
+        onSearch={(data) => {
+          // 重置分页数据
+          actionRef.current?.reset?.();
+          console.log(data);
+          // setParam(data);
+        }}
+      />
+      <ProTable<UserItem>
+        actionRef={actionRef}
+        // params={param}
+        columns={columns}
+        search={false}
+        headerTitle={
+          <PermissionButton
+            onClick={() => {
+              // setMode('add');
+            }}
+            isPermission={permission.add}
+            key="button"
+            icon={<PlusOutlined />}
+            type="primary"
+          >
+            {intl.formatMessage({
+              id: 'pages.data.option.add',
+              defaultMessage: '新增',
+            })}
+          </PermissionButton>
+        }
+        // request={async (params) =>
+        //   service.query({ ...params, sorts: [{ name: 'createTime', order: 'desc' }] })
+        // }
+      />
+    </PageContainer>
+  );
+};
+export default Opcua;

+ 4 - 0
src/pages/link/Channel/Opcua/typings.d.ts

@@ -0,0 +1,4 @@
+type Item = {
+  id: string;
+  name: string;
+};

+ 23 - 1
src/utils/menu/index.ts

@@ -40,7 +40,29 @@ const extraRouteObj = {
     children: [{ code: 'AMap', name: '地图' }],
   },
 };
-
+//额外路由
+export const extraRouteArr = [
+  {
+    code: 'account',
+    id: 'accout',
+    name: '个人中心',
+    url: '/account',
+    hideInMenu: true,
+    children: [
+      {
+        code: 'account/Center',
+        name: '基本设置',
+        url: '/account/center',
+      },
+      {
+        code: 'account/Center/bind',
+        name: '第三方页面',
+        url: '/account/center/bind',
+        hideInMenu: true,
+      },
+    ],
+  },
+];
 /**
  * 根据url获取映射的组件
  * @param files

+ 3 - 0
src/utils/menu/router.ts

@@ -35,6 +35,7 @@ export enum MENUS_CODE {
   'link/Certificate' = 'link/Certificate',
   'link/Gateway' = 'link/Gateway',
   'link/Opcua' = 'link/Opcua',
+  'link/Channal/Opcua' = 'link/Channal/Opcua',
   'link/Protocol/Debug' = 'link/Protocol/Debug',
   'link/Protocol' = 'link/Protocol',
   'link/Type' = 'link/Type',
@@ -109,6 +110,8 @@ export enum MENUS_CODE {
   'system/Menu/Detail' = 'system/Menu/Detail',
   'system/Department/Detail' = 'system/Department/Detail',
   'link/Type/Detail' = 'link/Type/Detail',
+  'account/Center' = 'account/Center',
+  'account/Center/bind' = 'account/Center/bind',
   'Northbound/DuerOS' = 'Northbound/DuerOS',
   'Northbound/AliCloud' = 'Northbound/AliCloud',
   'Northbound/AliCloud/Detail' = 'Northbound/AliCloud/Detail',