You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@inlong.apache.org by do...@apache.org on 2021/07/13 04:38:23 UTC

[incubator-inlong] 03/10: feat(broker): broker complete

This is an automated email from the ASF dual-hosted git repository.

dockerzhang pushed a commit to branch new-web-client
in repository https://gitbox.apache.org/repos/asf/incubator-inlong.git

commit 47b69ec5c4a90fe9a0d2a4689a811fcb42235469
Author: zakwu <12...@qq.com>
AuthorDate: Fri Jun 19 15:44:50 2020 +0800

    feat(broker): broker complete
---
 web/src/components/Breadcrumb/index.tsx        |   2 +-
 web/src/components/Layout/index.less           |   8 +-
 web/src/components/Modalx/index.less           |  17 +
 web/src/components/Modalx/index.tsx            |  49 +++
 web/src/components/Tablex/index.tsx            |   4 +-
 web/src/components/Tablex/tableFilterHelper.ts |  32 ++
 web/src/configs/menus/index.tsx                |   5 +-
 web/src/context/globalContext.ts               |   1 +
 web/src/pages/Broker/index.less                |   0
 web/src/pages/Broker/index.tsx                 | 482 +++++++++++++++++++++++++
 web/src/pages/Issue/consumeGroupDetail.tsx     |  16 +
 web/src/router.tsx                             |   5 +-
 web/src/routes/index.tsx                       |   4 +-
 web/src/utils/index.ts                         |  12 +
 14 files changed, 629 insertions(+), 8 deletions(-)

diff --git a/web/src/components/Breadcrumb/index.tsx b/web/src/components/Breadcrumb/index.tsx
index 4d41369..c4113bf 100644
--- a/web/src/components/Breadcrumb/index.tsx
+++ b/web/src/components/Breadcrumb/index.tsx
@@ -19,7 +19,7 @@ const BasicLayout: React.FC<BreadcrumbProps> = props => {
     const breadcrumbNameMap = {} as any;
     breadcrumbMap &&
       breadcrumbMap.forEach((t: MenuDataItem) => {
-        breadcrumbNameMap[t.pro_layout_parentKeys.join('/') + t.key] = t.name;
+        breadcrumbNameMap[t.key as string] = t.name;
       });
     const url = `/${pathSnippets.slice(0, index + 1).join('/')}`;
     if (appendParams && index === pathSnippets.length - 1) {
diff --git a/web/src/components/Layout/index.less b/web/src/components/Layout/index.less
index 9811a7d..4d0184e 100644
--- a/web/src/components/Layout/index.less
+++ b/web/src/components/Layout/index.less
@@ -13,9 +13,15 @@
 // global css
 .main-container {
   background: #fff;
-  padding: 20px;
+  padding: 10px 20px 20px 20px;
 }
 
 .search-wrapper {
   margin-bottom: 20px;
+}
+
+.options-wrapper {
+  a {
+    margin-right: 8px;
+  }
 }
\ No newline at end of file
diff --git a/web/src/components/Modalx/index.less b/web/src/components/Modalx/index.less
new file mode 100644
index 0000000..6b03518
--- /dev/null
+++ b/web/src/components/Modalx/index.less
@@ -0,0 +1,17 @@
+.psw-set {
+  border-top: 1px solid #ccc;
+  padding-top: 20px;
+
+  .pws-label {
+    float: left;
+    line-height: 32px;
+  }
+
+  .psw-input {
+    width: 300px;
+  }
+}
+
+.enhance {
+  color: red;
+}
\ No newline at end of file
diff --git a/web/src/components/Modalx/index.tsx b/web/src/components/Modalx/index.tsx
new file mode 100644
index 0000000..16a7831
--- /dev/null
+++ b/web/src/components/Modalx/index.tsx
@@ -0,0 +1,49 @@
+/**
+ * TABLE COMPONENT WITH SEARCH
+ */
+import { Modal, Input, Tooltip } from 'antd';
+import * as React from 'react';
+import { ModalProps } from 'antd/lib/modal';
+import { ReactElement, useEffect } from 'react';
+import './index.less';
+
+const { useState } = React;
+
+export interface OKProps {
+  e: React.MouseEvent<HTMLElement>;
+  psw: string;
+  params?: any;
+}
+
+type ComProps = {
+  context?: number;
+  children?: ReactElement;
+  onOk?: (p: OKProps) => {};
+  params?: any;
+};
+
+const Comp = (props: ComProps & Omit<ModalProps, 'onOk'>) => {
+  const { params } = props;
+  const [psw, setPsw] = useState('');
+  const onOk = (e: React.MouseEvent<HTMLElement>) => {
+    props.onOk && props.onOk({ e, psw, params });
+  };
+
+  return (
+    <>
+      <Modal {...props} className="textWrap" width="60%" onOk={onOk}>
+        {props.children}
+        <div className="psw-set">
+          <label className="pws-label">机器授权:</label>
+          <Input
+            className="psw-input"
+            placeholder="请输入机器授权字段,验证操作权限"
+            onChange={e => setPsw(e.target.value)}
+          />
+        </div>
+      </Modal>
+    </>
+  );
+};
+
+export default Comp;
diff --git a/web/src/components/Tablex/index.tsx b/web/src/components/Tablex/index.tsx
index dc5e904..6e311ad 100644
--- a/web/src/components/Tablex/index.tsx
+++ b/web/src/components/Tablex/index.tsx
@@ -21,6 +21,7 @@ interface ComProps extends TableProps<any> {
   defaultSearchKey?: string;
   isTruePagination?: boolean;
   showSearch?: boolean;
+  searchWidth?: number;
 }
 
 const Comp = (props: ComProps) => {
@@ -32,6 +33,7 @@ const Comp = (props: ComProps) => {
     defaultSearchKey,
     isTruePagination,
     showSearch = true,
+    searchWidth = 8,
   } = props;
   const [filterKey, setFilterKey] = useState(defaultSearchKey);
   // 自动增加排序
@@ -83,7 +85,7 @@ const Comp = (props: ComProps) => {
     <>
       {showSearch && filterFnX && (
         <Row gutter={20} className="mb10">
-          <Col span={8}>
+          <Col span={searchWidth} style={{ padding: 0 }}>
             <Tooltip title={filterKey}>
               <Search
                 value={filterKey}
diff --git a/web/src/components/Tablex/tableFilterHelper.ts b/web/src/components/Tablex/tableFilterHelper.ts
new file mode 100644
index 0000000..0032e8f
--- /dev/null
+++ b/web/src/components/Tablex/tableFilterHelper.ts
@@ -0,0 +1,32 @@
+interface TableFilterHelperProp {
+  key: string;
+  targetArray: Array<any>;
+  srcArray: Array<any>;
+  filterList: Array<any>;
+  updateFunction?: (p: Array<any>) => void;
+}
+const tableFilterHelper = (p: TableFilterHelperProp): any[] => {
+  const { key, srcArray = [], filterList, updateFunction } = p;
+  const res: any[] = [];
+
+  if (key) {
+    srcArray.forEach(it => {
+      const tar = filterList.map(t => {
+        return it[t];
+      });
+      let isFilterRight = false;
+      tar.forEach(t => {
+        if ((t + '').indexOf(key) > -1) isFilterRight = true;
+      });
+      if (isFilterRight) {
+        res.push(it);
+      }
+    });
+  }
+
+  if (updateFunction) updateFunction(res);
+
+  return res;
+};
+
+export default tableFilterHelper;
diff --git a/web/src/configs/menus/index.tsx b/web/src/configs/menus/index.tsx
index 140f583..e689571 100644
--- a/web/src/configs/menus/index.tsx
+++ b/web/src/configs/menus/index.tsx
@@ -28,13 +28,14 @@ const menus: Route[] = [
     name: '配置管理',
     key: '/other',
     icon: <SettingOutlined />,
+    path: '/other',
     children: [
       {
-        path: '/hello',
+        path: '/broker',
         name: 'Broker列表',
       },
       {
-        path: '/user',
+        path: '/topic',
         name: 'topic列表',
       },
     ],
diff --git a/web/src/context/globalContext.ts b/web/src/context/globalContext.ts
index 34e7f06..0540e02 100644
--- a/web/src/context/globalContext.ts
+++ b/web/src/context/globalContext.ts
@@ -6,6 +6,7 @@ export interface GlobalContextProps {
   setCluster?: Function;
   breadMap?: BreadcrumbProps['breadcrumbMap'];
   setBreadMap?: Function;
+  userInfo?: any;
 }
 
 export default React.createContext<GlobalContextProps>({});
diff --git a/web/src/pages/Broker/index.less b/web/src/pages/Broker/index.less
new file mode 100644
index 0000000..e69de29
diff --git a/web/src/pages/Broker/index.tsx b/web/src/pages/Broker/index.tsx
new file mode 100644
index 0000000..644f3e2
--- /dev/null
+++ b/web/src/pages/Broker/index.tsx
@@ -0,0 +1,482 @@
+import React, { useContext, useState } from 'react';
+import GlobalContext from '@/context/globalContext';
+import Breadcrumb from '@/components/Breadcrumb';
+import Table from '@/components/Tablex';
+import Modal, { OKProps } from '@/components/Modalx';
+import {
+  Form,
+  Select,
+  Button,
+  Spin,
+  Switch,
+  Input,
+  Row,
+  Col,
+  message,
+} from 'antd';
+import { useImmer } from 'use-immer';
+import { useRequest } from '@/hooks';
+import tableFilterHelper from '@/components/Tablex/tableFilterHelper';
+import { boolean2Chinese } from '@/utils';
+import './index.less';
+
+declare type BrokerData = any[];
+interface BrokerResultData {
+  acceptPublish: string;
+  acceptSubscribe: string;
+  brokerId: number;
+  brokerIp: string;
+  brokerPort: number;
+  brokerTLSPort: number;
+  brokerVersion: string;
+  enableTLS: boolean;
+  isAutoForbidden: boolean;
+  isBrokerOnline: string;
+  isConfChanged: string;
+  isConfLoaded: string;
+  isRepAbnormal: boolean;
+  manageStatus: string;
+  runStatus: string;
+  subStatus: string;
+  [key: string]: any;
+}
+
+const { Option } = Select;
+const OPTIONS = [
+  {
+    value: 'online',
+    name: '上线',
+  },
+  {
+    value: 'offline',
+    name: '下线',
+  },
+  {
+    value: 'reload',
+    name: '重载',
+  },
+  {
+    value: 'delete',
+    name: '删除',
+  },
+];
+const OPTIONS_VALUES = OPTIONS.map(t => t.value);
+const queryBroker = (data: BrokerResultData) => ({
+  url: '/api/op_query/admin_query_broker_run_status',
+  data: data,
+});
+
+const Broker: React.FC = () => {
+  // column config
+  const columns = [
+    {
+      title: 'BrokerID',
+      dataIndex: 'brokerId',
+      fixed: 'left',
+    },
+    {
+      title: 'BrokerIP',
+      dataIndex: 'brokerIp',
+    },
+    {
+      title: 'BrokerPort',
+      dataIndex: 'brokerPort',
+    },
+    {
+      title: '管理状态',
+      dataIndex: 'manageStatus',
+    },
+    {
+      title: '运行状态',
+      dataIndex: 'runStatus',
+    },
+    {
+      title: '运行子状态',
+      dataIndex: 'subStatus',
+    },
+    {
+      title: '可发布',
+      dataIndex: 'acceptPublish',
+      render: (t: string, r: BrokerResultData) => {
+        return (
+          <Switch
+            checked={t === 'true'}
+            onChange={e => onSwitchChange(e, r, 'acceptPublish')}
+          />
+        );
+      },
+    },
+    {
+      title: '可订阅',
+      dataIndex: 'acceptSubscribe',
+      render: (t: string, r: BrokerResultData) => {
+        return (
+          <Switch
+            checked={t === 'true'}
+            onChange={e => onSwitchChange(e, r, 'acceptSubscribe')}
+          />
+        );
+      },
+    },
+    {
+      title: '配置变更',
+      dataIndex: 'isConfChanged',
+      render: (t: string) => boolean2Chinese(t),
+    },
+    {
+      title: '变更加载',
+      dataIndex: 'isConfLoaded',
+      render: (t: string) => boolean2Chinese(t),
+    },
+    {
+      title: 'broker注册',
+      dataIndex: 'isBrokerOnline',
+      render: (t: string) => boolean2Chinese(t),
+    },
+    {
+      title: '上线',
+      dataIndex: 'isBrokerOnline',
+      render: (t: string) => boolean2Chinese(t),
+    },
+    {
+      title: 'TLS端口',
+      dataIndex: 'brokerTLSPort',
+      render: (t: string) => boolean2Chinese(t),
+    },
+    {
+      title: '启用TLS',
+      dataIndex: 'enableTLS',
+      render: (t: boolean) => boolean2Chinese(t),
+    },
+    {
+      title: '上报异常',
+      dataIndex: 'isRepAbnormal',
+      render: (t: boolean) => boolean2Chinese(t),
+    },
+    {
+      title: '自动屏蔽',
+      dataIndex: 'isAutoForbidden',
+      render: (t: boolean) => boolean2Chinese(t),
+    },
+    {
+      title: '操作',
+      dataIndex: 'brokerIp',
+      fixed: 'right',
+      width: 180,
+      render: (t: string, r: any) => {
+        return (
+          <span className="options-wrapper">
+            {OPTIONS.map(t => (
+              <a key={t.value} onClick={() => onOptionsChange(t.value, r)}>
+                {t.name}
+              </a>
+            ))}
+          </span>
+        );
+      },
+    },
+  ];
+  const { breadMap, userInfo } = useContext(GlobalContext);
+  const [modalParams, updateModelParams] = useImmer<any>({});
+  const [filterData, updateFilterData] = useImmer<any>({});
+  const [selectBroker, setSelectBroker] = useState<any>([]);
+  const [brokerList, updateBrokerList] = useImmer<BrokerData>([]);
+  const [form] = Form.useForm();
+  const [newBrokerForm] = Form.useForm();
+  // init query
+  const { data, loading, run } = useRequest<any, BrokerData>(queryBroker, {
+    onSuccess: data => {
+      updateBrokerList(d => {
+        Object.assign(d, data);
+      });
+    },
+  });
+  // render funcs
+  const renderBrokerOptions = () => {
+    const columns = [
+      {
+        title: 'Broker',
+        render: (t: string, r: BrokerResultData) => {
+          return `${r.brokerId}#${r.brokerIp}:${r.brokerPort}`;
+        },
+      },
+      {
+        title: 'BrokerIP',
+        dataIndex: 'brokerIp',
+      },
+      {
+        title: '管理状态',
+        dataIndex: 'manageStatus',
+      },
+      {
+        title: '运行状态',
+        dataIndex: 'runStatus',
+      },
+      {
+        title: '运行子状态',
+        dataIndex: 'subStatus',
+      },
+      {
+        title: '可发布',
+        render: (t: string) => boolean2Chinese(t),
+      },
+      {
+        title: '可订阅',
+        render: (t: string) => boolean2Chinese(t),
+      },
+    ];
+    const dataSource = data.filter((t: BrokerResultData) =>
+      modalParams.params.includes(t.brokerId)
+    );
+    return (
+      <Table
+        columns={columns}
+        dataSource={dataSource}
+        rowKey="brokerId"
+      ></Table>
+    );
+  };
+  const renderNewBroker = () => {
+    const brokerFormArr = [
+      {
+        name: 'brokerId',
+        defaultValue: '0',
+      },
+      {
+        name: 'numPartitions',
+        defaultValue: '3',
+      },
+      {
+        name: 'brokerIP',
+        defaultValue: '',
+      },
+      {
+        name: 'brokerPort',
+        defaultValue: '8123',
+      },
+      {
+        name: 'deleteWhen',
+        defaultValue: '0 0 6,18 * * ?',
+      },
+      {
+        name: 'deletePolicy',
+        defaultValue: 'delete,168h',
+      },
+      {
+        name: 'unflushThreshold',
+        defaultValue: '1000',
+      },
+      {
+        name: 'unflushInterval',
+        defaultValue: '10000',
+      },
+      {
+        name: 'acceptPublish',
+        defaultValue: 'true',
+      },
+      {
+        name: 'acceptSubscribe',
+        defaultValue: 'true',
+      },
+    ];
+
+    return (
+      <Form form={newBrokerForm}>
+        <Row gutter={24}>
+          {brokerFormArr.map((t, index) => (
+            <Col span={12} key={'brokerFormArr' + index}>
+              <Form.Item
+                labelCol={{ span: 12 }}
+                label={t.name}
+                name={t.name}
+                initialValue={t.defaultValue}
+              >
+                <Input />
+              </Form.Item>
+            </Col>
+          ))}
+        </Row>
+      </Form>
+    );
+  };
+  const renderBrokerStateChange = () => {
+    const { params } = modalParams;
+
+    return (
+      <div>
+        请确认<span className="enhance">{params.option}</span> ID:{' '}
+        <span className="enhance">{params.id}</span> 的 Broker?
+      </div>
+    );
+  };
+
+  // events
+  const onOpenModal = (type: string, title: string, params?: any) => {
+    updateModelParams(m => {
+      m.type = type;
+      m.params = params;
+      Object.assign(m, {
+        params,
+        visible: type,
+        title,
+        onOk: (p: OKProps) => onModelOk(type, p),
+        onCancel: () =>
+          updateModelParams(m => {
+            m.visible = false;
+          }),
+      });
+    });
+  };
+  // table event
+  const onSwitchChange = (e: boolean, r: BrokerResultData, type: string) => {
+    const index = data.findIndex(
+      (t: BrokerResultData) => t.brokerId === r.brokerId
+    );
+    updateBrokerList(d => {
+      d[index][type] = e + '';
+    });
+    let option = '';
+    if (type === 'acceptPublish') {
+      option = e ? '发布' : '禁止发布';
+    } else if (type === 'acceptSubscribe') {
+      option = e ? '订阅' : '禁止订阅';
+    }
+
+    onOpenModal('brokerStateChange', `请确认操作`, { option, id: r.brokerId });
+  };
+  const onBrokerTableSelectChange = (p: any[], rows: any[]) => {
+    setSelectBroker(p);
+  };
+
+  // modal event
+  const onOptionsChange = (type: string, r?: BrokerResultData) => {
+    if (!r && !selectBroker.length) {
+      form.resetFields();
+      return message.error('批量操作至少选择一列!');
+    }
+    onOpenModal(
+      type,
+      `确认进行【${OPTIONS.find(t => t.value === type)?.name}】操作?`,
+      [r?.brokerId]
+    );
+  };
+  const onModelOk = (type: string, p: OKProps) => {
+    switch (type) {
+      case 'newBroker':
+        return newBroker(p);
+      default:
+        return brokerOptions(type, p);
+    }
+  };
+
+  const newBrokerQuery = useRequest<any, any>(
+    data => ({ url: '/api/op_modify/admin_add_broker_configure', ...data }),
+    { manual: true }
+  );
+  const newBroker = (p: OKProps) => {
+    const values = newBrokerForm.getFieldsValue();
+    newBrokerQuery.run({
+      data: {
+        ...values,
+        confModAuthToken: p.psw,
+        createUser: userInfo.userName,
+      },
+    });
+  };
+
+  const brokerOptionsQuery = useRequest<any, any>(
+    (url, data) => ({ url, ...data }),
+    { manual: true }
+  );
+  const brokerOptions = (type: string, p: OKProps) => {
+    const { params } = p;
+    brokerOptionsQuery.run(`/api/op_modify/admin_${type}_broker_configure`, {
+      data: {
+        brokerId: params ? params?.join(',') : selectBroker.join(','),
+        confModAuthToken: p.psw,
+        createUser: userInfo.userName,
+      },
+    });
+  };
+
+  return (
+    <Spin spinning={loading}>
+      <Breadcrumb breadcrumbMap={breadMap}></Breadcrumb>
+      <div className="main-container">
+        <div
+          className="search-wrapper"
+          style={{ float: 'right', marginRight: '-16px' }}
+        >
+          <Form form={form} layout={'inline'}>
+            <Form.Item label="批量操作" name="optionType">
+              <Select
+                style={{ width: 120 }}
+                onChange={(v: string) => onOptionsChange(v)}
+                placeholder="请选择操作"
+              >
+                {OPTIONS.map(t => (
+                  <Option value={t.value} key={t.value}>
+                    {t.name}
+                  </Option>
+                ))}
+              </Select>
+            </Form.Item>
+            <Form.Item>
+              <Button
+                type="primary"
+                onClick={() => onOpenModal('newBroker', '新建Broker')}
+                style={{ margin: '0 10px 0 10px' }}
+              >
+                新增
+              </Button>
+              <Button type="primary" onClick={() => run()}>
+                刷新
+              </Button>
+            </Form.Item>
+          </Form>
+        </div>
+        <Table
+          rowSelection={{ onChange: onBrokerTableSelectChange }}
+          columns={columns}
+          dataSource={brokerList}
+          rowKey="brokerId"
+          searchPlaceholder="请输入关键字搜索"
+          searchWidth={12}
+          dataSourceX={filterData.list}
+          scroll={{ x: 2500 }}
+          filterFnX={value =>
+            tableFilterHelper({
+              key: value,
+              srcArray: data,
+              targetArray: filterData.list,
+              updateFunction: res =>
+                updateFilterData(filterData => {
+                  filterData.list = res;
+                }),
+              filterList: [
+                'brokerId',
+                'brokerIp',
+                'brokerPort',
+                'runStatus',
+                'subStatus',
+                'manageStatus',
+              ],
+            })
+          }
+        ></Table>
+      </div>
+      <Modal {...modalParams}>
+        <div>
+          {modalParams.type &&
+            OPTIONS_VALUES.includes(modalParams.type) &&
+            renderBrokerOptions()}
+          {modalParams.type === 'newBroker' && renderNewBroker()}
+          {modalParams.type === 'brokerStateChange' &&
+            renderBrokerStateChange()}
+        </div>
+      </Modal>
+    </Spin>
+  );
+};
+
+export default Broker;
diff --git a/web/src/pages/Issue/consumeGroupDetail.tsx b/web/src/pages/Issue/consumeGroupDetail.tsx
index 9e54f42..8b2f562 100644
--- a/web/src/pages/Issue/consumeGroupDetail.tsx
+++ b/web/src/pages/Issue/consumeGroupDetail.tsx
@@ -2,6 +2,7 @@ import React, { useContext } from 'react';
 import GlobalContext from '@/context/globalContext';
 import Breadcrumb from '@/components/Breadcrumb';
 import Table from '@/components/Tablex';
+import tableFilterHelper from '@/components/Tablex/tableFilterHelper';
 import { Form, Input, Button, Spin } from 'antd';
 import { useImmer } from 'use-immer';
 import './index.less';
@@ -43,6 +44,7 @@ const ConsumeGroupDetail: React.FC = () => {
   const { breadMap } = useContext(GlobalContext);
   const [form] = Form.useForm();
   const [formValues, updateFormValues] = useImmer<any>({});
+  const [filterData, updateFilterData] = useImmer<any>({});
   const { data, loading, run } = useRequest<any, ConsumeGroupData>(
     () =>
       queryUser({
@@ -86,6 +88,20 @@ const ConsumeGroupDetail: React.FC = () => {
           columns={columns}
           dataSource={data?.list}
           rowKey="brokerAddr"
+          searchPlaceholder="请输入 broker地址/分区ID 搜索"
+          dataSourceX={filterData.list}
+          filterFnX={value =>
+            tableFilterHelper({
+              key: value,
+              srcArray: data?.list,
+              targetArray: filterData.list,
+              updateFunction: res =>
+                updateFilterData(filterData => {
+                  filterData.list = res;
+                }),
+              filterList: ['brokerAddr', 'partId'],
+            })
+          }
         ></Table>
       </div>
     </Spin>
diff --git a/web/src/router.tsx b/web/src/router.tsx
index 1b6a6e9..66e0cdc 100644
--- a/web/src/router.tsx
+++ b/web/src/router.tsx
@@ -14,10 +14,13 @@ import GlobalContext from '@/context/globalContext';
 const App = () => {
   const [cluster, setCluster] = useState();
   const [breadMap, setBreadMap] = useState();
+  const [userInfo, setUserInfo] = useState({
+    userName: 'webapi',
+  });
 
   return (
     <GlobalContext.Provider
-      value={{ cluster, setCluster, breadMap, setBreadMap }}
+      value={{ cluster, setCluster, breadMap, setBreadMap, userInfo }}
     >
       <Router>
         <Layout>
diff --git a/web/src/routes/index.tsx b/web/src/routes/index.tsx
index 032a633..7cbd3d6 100644
--- a/web/src/routes/index.tsx
+++ b/web/src/routes/index.tsx
@@ -10,8 +10,8 @@ const routes: RouteProps[] = [
     component: () => import('@/pages/Issue'),
   },
   {
-    path: '/hello',
-    component: () => import('@/pages/Other/Hello'),
+    path: '/broker',
+    component: () => import('@/pages/Broker'),
   },
   {
     path: '/user',
diff --git a/web/src/utils/index.ts b/web/src/utils/index.ts
index 0c6378b..aadcc68 100644
--- a/web/src/utils/index.ts
+++ b/web/src/utils/index.ts
@@ -27,3 +27,15 @@ export const isEmptyParam = (value: any): boolean => {
   // value为默认值
   return !value;
 };
+
+export const boolean2Chinese = (value: boolean | string): string => {
+  let v = false;
+  if (value === 'false') {
+    v = false;
+  } else if (value === 'true') {
+    v = true;
+  } else {
+    v = value as boolean;
+  }
+  return v === false ? '否' : '是';
+};