/huxy-admin

Huxy Admin is a customizable admin dashboard template based on React18. Built with Webpack5, @huxy/pack, useRouter, useStore, etc.

Primary LanguageJavaScriptMIT LicenseMIT

Huxy Admin

GitHub license npm version GitHub Workflow Status (with branch)

Huxy Admin is a customizable admin dashboard template based on React18. Built with Webpack5, @huxy/pack, useRouter, useStore, etc.

Huxy Admin is a customizable admin template for efficient and convenient development of maintainable and scalable systems, providing customized software design and development.

Huxy Admin uses MERN full-stack technology to quickly develop responsive web applications.

Huxy Admin 是一个可定制的后台管理模版,用于高效、便捷地开发可维护性和可扩展性强的系统,提供定制化软件设计与开发。Huxy Admin 使用 MERN 全栈技术,可快速开发响应式 Web 应用。

  1. 主题配置 ✅
  2. i18n配置 ✅
  3. 路由配置 ✅
  4. 权限管理 ✅
  5. API管理 ✅
  6. 项目管理 ✅
  7. 角色管理 ✅
  8. 页面监控 ✅
  9. 页面日志 ✅
  10. 会员管理 ✅
  11. 订单管理 ✅
  12. 消息管理 ✅
  13. 邮件管理 ✅
  14. 页面管理 ✅
  15. 文件管理 ✅
  16. 文档管理 ✅
  17. 需求管理 ✅

运行

依赖安装:pnpm i

项目运行

可自行配置运行项目,如运行 template 项目:

npm start --dirname=template

运行 blog 目录下的 router 项目:

npm start --dirname=blog/router

默认运行 app 目录。npm start

其它

其它一些命令:主要有 打包、文件分析、运行打包的资源、jest 测试、eslint、stylelint、lint-fix、prettier、release 等。

"start": "nodemon scripts/index.js --watch scripts/index.js",
"build": "webpack --config scripts/webpack.production.js --progress",
"analyze": "ANALYZE=1 webpack --config scripts/webpack.production.js --progress",
"server": "nodemon scripts/server.js --watch scripts/server.js",
"test": "jest --colors --coverage",
"eslint": "eslint 'app/**/*.{js,jsx}'",
"stylelint": "stylelint 'app/**/*.{css,less}'",
"lint": "npm run eslint && npm run stylelint",
"lint-fix": "eslint --fix 'app/**/*.{js,jsx}' && stylelint --fix 'app/**/*.{css,less}'",
"prettier": "prettier 'app/**/*' --write --ignore-unknown && npm run lint-fix",
"release": "standard-version",
  • npm run build:打包
  • npm run analyze:打包文件分析
  • npm test:单元测试
  • npm run lint:代码规范检查,包含 eslintstylelint
  • npm run lint-fix:代码规范修正
  • npm run prettier:代码美化
  • npm run release:自动打 tag 并生成 CHANGELOG

提供了 git 代码提交钩子 huskygit 提交信息规范 commitlint ,支持 pnpm 安装依赖。

全局配置

全局配置

主要有 运行端口、资源路径、打包路径、basepath、代理、mock 等。

configs/app.js

const app = {
  HOST: process.env.IP || 'http://localhost',
  PORT: process.env.PORT || 9500, // 开发环境运行端口
  PRO_PORT: process.env.PRO_PORT || 9501, // 打包运行端口
  BUILD_DIR: './build', // 打包路径
  PUBLIC_DIR: '../public', // 静态资源路径
  DEV_ROOT_DIR: '/', // dev basepath
  PRD_ROOT_DIR: '/', // prod basepath
  PROXY: {
    url: 'http://api.ihuxy.com', // 服务代理
    prefix: '/api', // 代理前缀
  },
  MOCK: 'http://localhost:9502', // mock 地址
  SERVER_PORT: 9503, // nodejs 本地服务端口
  projectName: '...', // 项目名称
};

项目配置

主要是路由、导航栏、主题、i18ns 等配置。

app/configs

环境配置

路由类型、basepath 通过全局配置获取。

export const browserRouter = !process.env.isDev;

export const basepath = browserRouter ? PRD_ROOT_DIR : DEV_ROOT_DIR;

路由

app/configs/router:路由钩子 beforeRender 可控制路由跳转。

const beforeRender = (input, next) => {
  const {path, prevPath} = input;
  const validPath = path.split('?')[0];
  if (validPath === initPath) {
    return next({path: '/'});
  }
  if (!token && !whiteRoutes.includes(validPath)) {
    return next({path: '/user/signin'});
  }
  next();
};

export default {
  browserRouter,
  beforeRender,
  basepath,
};

导航栏

app/configs/nav:导航栏可分为左侧导航栏和右侧导航栏。

export const leftNav = () => {
  const left = getIntls('nav.left', {});
  return [
    {
      key: 'collapse',
      name: left?.collapse ?? 'collapse',
      type: 'collapse',
      Custom: () => <CustomCollapse />,
    },
  ];
};
export const rightNav = () => {
  const right = getIntls('nav.right', {});
  return [
    {
      key: 'username',
      name: user?.name ?? right?.user,
      img: user?.avatar ?? defUser,
      children: [
        {
          key: 'profile',
          name: right?.profile ?? '个人中心',
          icon: 'UserOutlined',
          path: '/profile',
        },
        {
          key: 'settings',
          name: right?.settings ?? '设置',
          icon: 'SettingOutlined',
          path: '/profile',
        },
        {
          divider: true,
          key: 'logout',
          name: right?.logout ?? '退出',
          icon: 'PoweroffOutlined',
          handle: item => {
            logout();
          },
        },
      ],
    },
  ];
};

主题

app/configs/theme:主题列表包含主题名和主题配置表等。

const themeList = getIntls => [
  {
    name: getIntls('theme.dark', 'dark'),
    key: 'dark',
    list: dark,
    type: 'theme',
  },
  {
    name: getIntls('theme.dark1', 'dark1'),
    key: 'dark1',
    list: dark1,
    type: 'theme',
  },
  {
    name: getIntls('theme.dark', 'light'),
    key: 'light',
    list: light,
    type: 'theme',
  },
  {
    name: getIntls('theme.light1', 'light1'),
    key: 'light1',
    list: light1,
    type: 'theme',
  },
  {
    name: getIntls('theme.portal', 'portal'),
    key: 'portal',
    list: portal,
    type: 'theme',
  },
];

语言

app/configs/i18ns

const langList = [
  {
    key: 'zh',
    name: '汉语',
    icon: zh_icon,
  },
  {
    key: 'en',
    name: '英语',
    icon: en_icon,
  },
  {
    key: 'jp',
    name: '日语',
    icon: jp_icon,
  },
];

Api 请求

app/apis

请求参数处理

可自行配置路径 prefixheaderstokencredentials 等。

const getToken = () => ({Authorization: `test ${storage.get('token') || ''}`});

const fetch = ({method, url, ...opt}) => fetchApi(method)(`${TARGET}${url}`, {...opt, headers: getToken(), credentials: 'omit'});

返回结果处理

根据约定返回 code 码配置不同操作。可设置通用提示信息、数据格式化等。

const success_code = [200, 10000];

const handler = response => {
  return response
    .json()
    .then(result => {
      result.code = result.code ?? response.status;
      result.msg = result.message ?? result.msg ?? response.statusText;
      const {msg, code} = result;
      if (code === 401) {
        message.error(msg);
        logout(true);
        throw {code, message: msg};
      }
      if (!success_code.includes(code)) {
        throw {code, message: msg};
      }
      return result;
    })
    .catch(error => {
      message.error(error.message);
      throw error.message;
    });
};

接口配置

const apiList = [
  {
    name: 'login',
    url: '/auth/signup',
    method: 'post',
  },
  {
    fnName: 'getProfile',
    url: '/users/profile',
  },
];
  • fnName:自定义前端调用的函数名
  • name:默认前端调用的函数名 ${name}Fn
  • url:接口地址
  • method:请求方式,默认 get
  • type:数据类型,json\formData\form\query,默认 json。

可根据后台提供 API 文档来配置。schema 数据,方便同构。

路由

app/routes

支持动态路由、权限路由、路由白名单等。

基本配置

{
  path: '/demo',
  name: 'demo',
  icon: 'MergeCellsOutlined',
  denied: !isDev,
  component: () => import('@app/views/demo'),
},

可通过 denied 属性来控制是否渲染路由。可将 staticRoutes 设置为白名单路由。

可根据后台返回路由配置来做控制,也可根据后台返回权限码来控制。白名单一般就是登录注册页面,也可自行配置。

const allRoutes = [
  {
    path: '/',
    component: () => import('@app/commons/layout'),
    children: dynamicRoutes,
  },
  ...staticRoutes,
];

组件加载方式

路由组件可按需加载,可根据目录地址加载。

export const playgroundRoutes = {
  path: '/playground',
  name: ' Playground',
  icon: 'ConsoleSqlOutlined',
  children: [
    {
      path: '/demo',
      name: 'demo',
      icon: 'MergeCellsOutlined',
      denied: browserRouter,
      component: () => import('@app/views/demo'),
    },
    {
      path: '/icons',
      name: 'icons',
      icon: 'PictureOutlined',
      componentPath: '/demo/icons',
    },
    {
      path: '/panel',
      name: 'panel',
      component: PanelDemo,
    },
  ],
};

详情见简单实现 react router

useRouter 使用:

const output = useRouter(input);

通过传入一些配置(主要是路由类型、路由列表、路由拦截函数等),返回给我们当前路径下的渲染组件以及 menu 菜单的处理数据和面包屑数据。

提供 LinkuseRouteLink 为路由跳转组件,useRoute 可获取到当前路由下的信息。

详情见useRouter 使用

主题配置

app/theme

const sizes = {
  '--maxWidth': '100vw',
  '--menuWidth': '240px',
  '--collapseWidth': '68px',
  '--collapseMenuWidth': '180px',
  '--headerHeight': '68px',
  '--footerHeight': '60px',
  '--breadHeight': '50px',
  '--menuItemHeight': '48px',
};
const colors = {
  '--bannerBgColor': '#37424c',
  '--navBgColor': '#3c4752',
  '--menuBgColor': '#37424c',
  '--appBgColor': '#303841',
  '--pageBgColor': '#303841',
  '--panelBgColor': '#36404a',
  '--appColor': '#8c98a5',
  '--linkColor': '#9097a7',
  '--pageLinkColor': '#8c98a5',
  '--linkHoverColor': '#c8cddc',
  '--linkActiveColor': '#ffffff',
  '--borderColor': '#424e5a',
};

通过 css 变量自行设置大小和颜色,在系统中直接使用即可。

layout 配置

layout 可自己设计,整体框架无非就是头部导航栏和侧边菜单栏。

我提供了一个管理平台 layout 模板,菜单通过路由返回 menu 数据渲染,头部导航栏可根据 nav 配置数据渲染,面包屑也在路由返回数据 current 里面。

通用配置

commons/layout

const layoutConfigs = {
  MainTop: () => {},
  MenuBottom: () => {},
  Link: () => {},
  handleNavClick: () => {},
  logo: '',
  leftList: [],
  rightList: [],
  headerStyle: {},
  menuStyle: {},
  ...
};

全局 UI i18n 配置

const Index = props => (
  <UiI18n>
    <Layout {...props} />
  </UiI18n>
);

i18ns 配置

app/i18ns

加载

i18ns 目录存放语言包,以语言 key 命名,通过 key 值按需加载语言包。

const getI18n = async () => {
  const language = getLang();
  let i18ns = await import(`@app/i18ns/${language}`);
  i18ns = i18ns.default ?? i18ns;
  return {i18ns, language};
};

语言包配置,可以使用页面或模块命名文件,然后分别做配置,如:

  • zh
    • main
    • nav
    • router
    • theme
    • login

具体配置

const layout = {
  saveConfig: '保存配置',
  copyConfig: '拷贝配置',
  menuType: '菜单类型:',
  vertical: '纵向',
  horizontal: '横向',
  compose: '横纵组合',
  fontSize: '字体大小:',
  layoutDesign: '布局',
  sizeDesign: '大小',
  colorDesign: '颜色',
  save_cfg_msg: '主题保存成功!',
  copy_cfg_msg: '主题拷贝成功!',
  data_valid_msg: '请输入合法数据!',
  data_px_msg: '请输入500-5000内数据!',
  data_percent_msg: '请输入50-100内数据!',
  menu_width_msg: '请输入0-300内数据!',
};

使用

提供 3 种使用方式:

组件

<Intls keys="login.email">邮箱</Intls>

通过 key 来获取语言数据,如果获取失败则展示 children 文本。

hook

const getIntls = useIntls();
message.success(getIntls('main.layout.save_cfg_msg', '成功!'));

hook 返回一个函数 getIntls ,第一个参数为文本 key 值,第二个参数是返回为空时的默认值。

函数

getIntls 函数和 useIntls 返回的函数一致,不同的是此函数可在任何地方使用,如纯 js 代码里。

状态管理

app/store

基于 flux 理念,使用发布订阅模式来实现。主要提供 3 个函数:

  • getState:获取数据
  • setState:设置数据
  • subscribe:监听数据

通用方式

const {useStore} = props;

const [list, update, subscribe] = useStore('userList', {});
  • key:userList
  • 默认值:{}
  • list:value 值
  • update:更新函数
  • subscribe:监听函数
const Page1 = props => {
  const [list, update] = useStore('userList', []);
  const deleteUse = async id => {
    await fetchDel({id});
    update();
  };
};

const Page2 = props => {
  const [, , subscribe] = useStore('userList', []);
  useEffect(() => {
    subscribe(result => {
      console.log(result);
    });
  }, []);
};

组合

有 2 种使用方式,函数和 hook。当创建时使用的是同一个 store 时,二者数据可通用。如:

// a.jsx 可检测到数据更新
const [state] = useStore('store-test');
// b.jsx 设置数据
store.setState({'store-test', 'test data'});

创建 store

import createStore from '@huxy/utils/createStore';
import createContainer from '@huxy/utils/createContainer';
import createUseContainer from '@huxy/use/createContainer';

export const container = createStore();

export const store = createContainer(container);
export const useStore = createUseContainer(container);

数据名(key)

export const langName = 'lang-store';
export const themeName = 'theme-store';
export const menuTypeName = 'menuType-store';
export const i18nsName = 'i18ns-store';
export const userInfoName = 'userInfo-store';
export const permissionName = 'permission-store';
export const routersName = 'routers-store';

创建函数(hooks)

export const langStore = store(langName);
export const themeStore = store(themeName);
export const menuTypeStore = store(menuTypeName);
export const i18nsStore = store(i18nsName);
export const userInfoStore = store(userInfoName);
export const permissionStore = store(permissionName);

export const useLangStore = initState => useStore(langName, initState);
export const useThemeStore = initState => useStore(themeName, initState);
export const useMenuTypeStore = initState => useStore(menuTypeName, initState);
export const useI18nsStore = initState => useStore(i18nsName, initState);
export const useUserInfoStore = initState => useStore(userInfoName, initState);
export const usePermissionStore = initState => useStore(permissionName, initState);

使用

// useI18nsStore
const Intls = ({keys, children}) => {
  const [i18ns] = useI18nsStore();
  return (keys && i18ns?.getValue(keys)) ?? children ?? '';
};
// i18nsStore
export const getIntls = (keys, def) => (keys && i18nsStore.getState()?.getValue(keys)) ?? def ?? '';

utils、components、hooks

组件可分为基础组件、业务组件,基础组件粒度低、通用性高,业务组件包含了一些业务场景,在某一领域模型内通用,通常是基础组件组合而来。

utils

例如:格式化树

// formatTree
const fixIcon = router =>
  router.map(item => {
    item.key = item.key || item.path;
    item.icon = <Icon icon={item.iconKey || 'EyeInvisibleOutlined'} />;
    return item;
  });

const formatTree = arr =>
  traverItem(item => {
    if (!isValidArr(item.children)) {
      item.isLeaf = true;
    }
  })(arr2TreeByPath(fixIcon(arr)));

export default formatTree;

详情见 utils

components

例如:超出宽度文本自动省略并展示 tooltip

const style = {
  overflow: 'hidden',
  textOverflow: 'ellipsis',
  whiteSpace: 'nowrap',
  display: 'inline-block',
  width: '100%',
};

const Ellipsis = ({children, ttProps}) => {
  const span = useRef();
  const [ellipsis, setEllipsis] = useState(false);
  const state = useEleResize(span, 250);
  useEffect(() => {
    if (span.current) {
      const {width: tWidth} = getTextSize(children);
      const {width} = getPosition(span.current);
      const isEllipsis = tWidth > width;
      if (isEllipsis !== ellipsis) {
        setEllipsis(isEllipsis);
      }
    }
  }, [children, state.width]);
  return (
    <span ref={span} style={style}>
      {ellipsis ? (
        <Tooltip placement="topLeft" title={children} {...ttProps}>
          {children}
        </Tooltip>
      ) : (
        <span>{children}</span>
      )}
    </span>
  );
};

Input

const Input = ({className, ...rest}) => {
  const cls = ['h-input', ...(className?.split(' ') ?? [])]
    .filter(Boolean)
    .map(c => styles[c])
    .join(' ');
  return <input className={cls} {...rest} />;
};

export default Input;

Radio

const Radio = ({options, name, value: checked, onChange, ...rest}) => (
  <div className="radio-wrap" {...rest} style={{display: 'flex'}}>
    {options.map(({value, label}) => (
      <div key={value} className="radio-item" onClick={e => onChange?.(value, e)} style={{display: 'flex', alignItems: 'center', marginRight: '12px', cursor: 'pointer'}}>
        <input type="radio" name={name} value={value} checked={value === checked} readOnly />
        <span style={{marginLeft: '6px'}}>{value}</span>
      </div>
    ))}
  </div>
);

export default Radio;

Tooltip

const Tooltip = ({children, title, placement}) => (
  <span className={`tooltip-${placement}`} tooltips={title}>
    {children}
  </span>
);

export default Tooltip;

Drawer

const Drawer = ({visible, onClose, footer, header, className, children, width = '300px'}) => {
  const cls = ['drawer-wrap', visible ? 'open' : '', ...(className?.split(' ') ?? [])]
    .filter(Boolean)
    .map(c => styles[c])
    .join(' ');
  return (
    <Mask open={visible} close={onClose} delay={250} hasMask={true} className="huxy-drawer">
      <div className={cls} style={{width}}>
        <div className={styles['drawer-container']}>
          <div className={styles['drawer-header']}>
            <a className={styles['ico-close']} onClick={onClose} />
            {header}
          </div>
          <div className={styles['drawer-content']}>{children}</div>
          {footer ? <div className={styles['drawer-footer']}>{footer}</div> : null}
        </div>
      </div>
    </Mask>
  );
};

export default Drawer;

详情见 components

hooks

例如:请求列表数据 hook ,包含返回结果处理、分页、搜索、更新等。

useFetchList:

const useFetchList = (fetchList, initParams = null, handleResult, commonParams = null) => {
  const [result, updateResult] = useAsync({});
  const update = useCallback(params => updateResult({res: fetchList({...commonParams, ...params})}, handleResult), []);
  useEffect(() => {
    update({...initParams});
  }, []);
  const {res} = result;
  const isPending = !res || res.pending;

  return [{isPending, data: res?.result}, update];
};

useHandleList:

const useHandleList = (fetchList, initParams = null, handleResult, commonParams = null) => {
  const {current, size, ...rest} = initParams || {};
  const search = useRef(rest || {});
  const page = useRef({current: current || 1, size: size || 10});
  const [result, update] = useFetchList(fetchList, {...page.current, ...search.current}, handleResult, commonParams);

  const pageChange = (current, size) => {
    page.current = {current, size};
    update({
      ...page.current,
      ...search.current,
    });
  };
  const searchList = values => {
    search.current = values;
    page.current = {...page.current, current: 1};
    update({...page.current, ...search.current});
  };
  const handleUpdate = params => {
    const {current, size, ...rest} = params;
    page.current = {current: current ?? page.current.current, size: size ?? page.current.size};
    search.current = {...search.current, ...rest};
    update({...page.current, ...search.current});
  };

  return [result, handleUpdate, pageChange, searchList];
};

详情见 use