{"id":21036552,"url":"https://github.com/ahyiru/huxy-admin","last_synced_at":"2025-04-05T13:08:29.614Z","repository":{"id":59364525,"uuid":"478600239","full_name":"ahyiru/huxy-admin","owner":"ahyiru","description":"Huxy Admin is a customizable admin dashboard template based on React. Built with Webpack, @huxy/pack, useRouter, useStore, etc.","archived":false,"fork":false,"pushed_at":"2025-01-15T11:43:23.000Z","size":17388,"stargazers_count":151,"open_issues_count":0,"forks_count":3,"subscribers_count":2,"default_branch":"develop","last_synced_at":"2025-03-29T12:08:54.473Z","etag":null,"topics":["dashboard","express","full-stack","hooks","husky","low-code","mongoose","nodejs","react","responsive","template","webgl","webpack"],"latest_commit_sha":null,"homepage":"https://yiru.gitee.io/huxy-admin","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ahyiru.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-04-06T14:39:57.000Z","updated_at":"2025-01-15T11:43:24.000Z","dependencies_parsed_at":"2023-02-19T04:01:09.228Z","dependency_job_id":"2f5b5d92-71cd-4725-b705-0cbf2706607d","html_url":"https://github.com/ahyiru/huxy-admin","commit_stats":null,"previous_names":[],"tags_count":11,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ahyiru%2Fhuxy-admin","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ahyiru%2Fhuxy-admin/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ahyiru%2Fhuxy-admin/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ahyiru%2Fhuxy-admin/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ahyiru","download_url":"https://codeload.github.com/ahyiru/huxy-admin/tar.gz/refs/heads/develop","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247339158,"owners_count":20923014,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dashboard","express","full-stack","hooks","husky","low-code","mongoose","nodejs","react","responsive","template","webgl","webpack"],"created_at":"2024-11-19T13:20:48.999Z","updated_at":"2025-04-05T13:08:29.576Z","avatar_url":"https://github.com/ahyiru.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"## Huxy Admin\n\n[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/ahyiru/huxy-admin/blob/develop/LICENSE)\n[![npm version](https://img.shields.io/npm/v/@huxy/router.svg)](https://www.npmjs.com/package/@huxy/router)\n[![GitHub Workflow Status (with branch)](https://img.shields.io/github/actions/workflow/status/ahyiru/huxy-admin/lint-ci.yml?branch=develop)](https://github.com/ahyiru/huxy-admin/actions/workflows/lint-ci.yml)\n[![](https://img.shields.io/badge/blog-ihuxy-blue.svg)](http://ihuxy.com/)\n\nHuxy Admin is a customizable admin dashboard template based on React19. Built with  Webpack5, [@huxy/pack](https://www.npmjs.com/package/@huxy/pack), [useRouter](https://www.npmjs.com/package/@huxy/router), [useStore](https://www.npmjs.com/package/@huxy/use), etc.\n\nHuxy Admin is a customizable admin template for efficient and convenient development of maintainable and scalable systems, providing customized software design and development.\n\nHuxy Admin uses MERN full-stack technology to quickly develop responsive web applications.\n\nHuxy Admin 是一个可定制的后台管理模版，用于高效、便捷地开发可维护性和可扩展性强的系统，提供定制化软件设计与开发。Huxy Admin 使用 MERN 全栈技术，可快速开发响应式 Web 应用。\n\n### [系统功能](https://github.com/ahyiru/huxy-admin/blob/develop/doc/functions.md)\n\n1. 主题配置 ✅\n2. i18n配置 ✅\n3. 路由配置 ✅\n4. 权限管理 ✅\n5. API管理 ✅\n6. 项目管理 ✅\n7. 角色管理 ✅\n8. 页面监控 ✅\n9. 页面日志 ✅\n10. 会员管理 ✅\n11. 订单管理 ✅\n12. 消息管理 ✅\n13. 邮件管理 ✅\n14. 页面管理 ✅\n15. 文件管理 ✅\n16. 文档管理 ✅\n17. 需求管理 ✅\n\n### 运行\n\n#### 依赖安装：`pnpm i`\n\n#### 项目运行\n\n可自行配置运行项目，如运行 `template` 项目：\n\n```js\nnpm start --dirname=template\n\n```\n\n运行 `blog` 目录下的 `router` 项目：\n\n```js\nnpm start --dirname=blog/router\n\n```\n\n默认运行 `app` 目录。`npm start`。\n\n#### 其它\n\n其它一些命令：主要有 打包、文件分析、运行打包的资源、jest 测试、eslint、stylelint、lint-fix、prettier、release 等。\n\n```json\n\"start\": \"nodemon scripts/index.js --watch scripts/index.js\",\n\"build\": \"webpack --config scripts/webpack.production.js --progress\",\n\"analyze\": \"ANALYZE=1 webpack --config scripts/webpack.production.js --progress\",\n\"server\": \"nodemon scripts/server.js --watch scripts/server.js\",\n\"test\": \"jest --colors --coverage\",\n\"eslint\": \"eslint 'app/**/*.{js,jsx}'\",\n\"stylelint\": \"stylelint 'app/**/*.{css,less}'\",\n\"lint\": \"npm run eslint \u0026\u0026 npm run stylelint\",\n\"lint-fix\": \"eslint --fix 'app/**/*.{js,jsx}' \u0026\u0026 stylelint --fix 'app/**/*.{css,less}'\",\n\"prettier\": \"prettier 'app/**/*' --write --ignore-unknown \u0026\u0026 npm run lint-fix\",\n\"release\": \"standard-version\",\n\n```\n\n- `npm run build`：打包\n- `npm run analyze`：打包文件分析\n- `npm test`：单元测试\n- `npm run lint`：代码规范检查，包含 `eslint` 和 `stylelint`\n- `npm run lint-fix`：代码规范修正\n- `npm run prettier`：代码美化\n- `npm run release`：自动打 `tag` 并生成 `CHANGELOG`\n\n提供了 `git` 代码提交钩子 `husky` ，`git` 提交信息规范 `commitlint` ，支持 `pnpm` 安装依赖。\n\n### 全局配置\n\n#### 全局配置\n\n主要有 运行端口、资源路径、打包路径、basepath、代理、mock 等。\n\n`configs/app.js`\n\n```js\nconst app = {\n  HOST: process.env.IP || 'http://localhost',\n  PORT: process.env.PORT || 9500, // 开发环境运行端口\n  PRO_PORT: process.env.PRO_PORT || 9501, // 打包运行端口\n  BUILD_DIR: './build', // 打包路径\n  PUBLIC_DIR: '../public', // 静态资源路径\n  DEV_ROOT_DIR: '/', // dev basepath\n  PRD_ROOT_DIR: '/', // prod basepath\n  PROXY: {\n    url: 'http://api.ihuxy.com', // 服务代理\n    prefix: '/api', // 代理前缀\n  },\n  MOCK: 'http://localhost:9502', // mock 地址\n  SERVER_PORT: 9503, // nodejs 本地服务端口\n  projectName: '...', // 项目名称\n};\n```\n\n### 项目配置\n\n主要是路由、导航栏、主题、i18ns 等配置。\n\n`app/configs`\n\n#### 环境配置\n\n路由类型、basepath 通过全局配置获取。\n\n```js\nexport const browserRouter = !process.env.isDev;\n\nexport const basepath = browserRouter ? PRD_ROOT_DIR : DEV_ROOT_DIR;\n```\n\n#### 路由\n\n`app/configs/router`：路由钩子 `beforeRender` 可控制路由跳转。\n\n```js\nconst beforeRender = (input, next) =\u003e {\n  const {path, prevPath} = input;\n  const validPath = path.split('?')[0];\n  if (validPath === initPath) {\n    return next({path: '/'});\n  }\n  if (!token \u0026\u0026 !whiteRoutes.includes(validPath)) {\n    return next({path: '/user/signin'});\n  }\n  next();\n};\n\nexport default {\n  browserRouter,\n  beforeRender,\n  basepath,\n};\n```\n\n#### 导航栏\n\n`app/configs/nav`：导航栏可分为左侧导航栏和右侧导航栏。\n\n```js\nexport const leftNav = () =\u003e {\n  const left = getIntls('nav.left', {});\n  return [\n    {\n      key: 'collapse',\n      name: left?.collapse ?? 'collapse',\n      type: 'collapse',\n      Custom: () =\u003e \u003cCustomCollapse /\u003e,\n    },\n  ];\n};\nexport const rightNav = () =\u003e {\n  const right = getIntls('nav.right', {});\n  return [\n    {\n      key: 'username',\n      name: user?.name ?? right?.user,\n      img: user?.avatar ?? defUser,\n      children: [\n        {\n          key: 'profile',\n          name: right?.profile ?? '个人中心',\n          icon: 'UserOutlined',\n          path: '/profile',\n        },\n        {\n          key: 'settings',\n          name: right?.settings ?? '设置',\n          icon: 'SettingOutlined',\n          path: '/profile',\n        },\n        {\n          divider: true,\n          key: 'logout',\n          name: right?.logout ?? '退出',\n          icon: 'PoweroffOutlined',\n          handle: item =\u003e {\n            logout();\n          },\n        },\n      ],\n    },\n  ];\n};\n```\n\n#### 主题\n\n`app/configs/theme`：主题列表包含主题名和主题配置表等。\n\n```js\nconst themeList = getIntls =\u003e [\n  {\n    name: getIntls('theme.dark', 'dark'),\n    key: 'dark',\n    list: dark,\n    type: 'theme',\n  },\n  {\n    name: getIntls('theme.dark1', 'dark1'),\n    key: 'dark1',\n    list: dark1,\n    type: 'theme',\n  },\n  {\n    name: getIntls('theme.dark', 'light'),\n    key: 'light',\n    list: light,\n    type: 'theme',\n  },\n  {\n    name: getIntls('theme.light1', 'light1'),\n    key: 'light1',\n    list: light1,\n    type: 'theme',\n  },\n  {\n    name: getIntls('theme.portal', 'portal'),\n    key: 'portal',\n    list: portal,\n    type: 'theme',\n  },\n];\n```\n\n#### 语言\n\n`app/configs/i18ns`：\n\n```js\nconst langList = [\n  {\n    key: 'zh',\n    name: '汉语',\n    icon: zh_icon,\n  },\n  {\n    key: 'en',\n    name: '英语',\n    icon: en_icon,\n  },\n  {\n    key: 'jp',\n    name: '日语',\n    icon: jp_icon,\n  },\n];\n```\n\n### Api 请求\n\n`app/apis`\n\n#### 请求参数处理\n\n可自行配置路径 `prefix` 、`headers`、`token` 、`credentials` 等。\n\n```js\nconst getToken = () =\u003e ({Authorization: `test ${storage.get('token') || ''}`});\n\nconst fetch = ({method, url, ...opt}) =\u003e fetchApi(method)(`${TARGET}${url}`, {...opt, headers: getToken(), credentials: 'omit'});\n```\n\n#### 返回结果处理\n\n根据约定返回 `code` 码配置不同操作。可设置通用提示信息、数据格式化等。\n\n```js\nconst success_code = [200, 10000];\n\nconst handler = response =\u003e {\n  return response\n    .json()\n    .then(result =\u003e {\n      result.code = result.code ?? response.status;\n      result.msg = result.message ?? result.msg ?? response.statusText;\n      const {msg, code} = result;\n      if (code === 401) {\n        message.error(msg);\n        logout(true);\n        throw {code, message: msg};\n      }\n      if (!success_code.includes(code)) {\n        throw {code, message: msg};\n      }\n      return result;\n    })\n    .catch(error =\u003e {\n      message.error(error.message);\n      throw error.message;\n    });\n};\n```\n\n#### 接口配置\n\n```js\nconst apiList = [\n  {\n    name: 'login',\n    url: '/auth/signup',\n    method: 'post',\n  },\n  {\n    fnName: 'getProfile',\n    url: '/users/profile',\n  },\n];\n```\n\n- fnName：自定义前端调用的函数名\n- name：默认前端调用的函数名 `${name}Fn`\n- url：接口地址\n- method：请求方式，默认 `get`\n- type：数据类型，json\\formData\\form\\query，默认 json。\n\n可根据后台提供 API 文档来配置。`schema` 数据，方便同构。\n\n### 路由\n\n`app/routes`\n\n支持动态路由、权限路由、路由白名单等。\n\n#### 基本配置\n\n```js\n{\n  path: '/demo',\n  name: 'demo',\n  icon: 'MergeCellsOutlined',\n  denied: !isDev,\n  component: () =\u003e import('@app/views/demo'),\n},\n```\n\n可通过 `denied` 属性来控制是否渲染路由。可将 `staticRoutes` 设置为白名单路由。\n\n可根据后台返回路由配置来做控制，也可根据后台返回权限码来控制。白名单一般就是登录注册页面，也可自行配置。\n\n```js\nconst allRoutes = [\n  {\n    path: '/',\n    component: () =\u003e import('@app/commons/layout'),\n    children: dynamicRoutes,\n  },\n  ...staticRoutes,\n];\n```\n\n#### 组件加载方式\n\n路由组件可按需加载，可根据目录地址加载。\n\n```js\nexport const playgroundRoutes = {\n  path: '/playground',\n  name: ' Playground',\n  icon: 'ConsoleSqlOutlined',\n  children: [\n    {\n      path: '/demo',\n      name: 'demo',\n      icon: 'MergeCellsOutlined',\n      denied: browserRouter,\n      component: () =\u003e import('@app/views/demo'),\n    },\n    {\n      path: '/icons',\n      name: 'icons',\n      icon: 'PictureOutlined',\n      componentPath: '/demo/icons',\n    },\n    {\n      path: '/panel',\n      name: 'panel',\n      component: PanelDemo,\n    },\n  ],\n};\n```\n\n详情见[简单实现 react router](https://zhuanlan.zhihu.com/p/106989011)\n\n#### useRouter 使用：\n\n```js\nconst output = useRouter(input);\n```\n\n通过传入一些配置（主要是路由类型、路由列表、路由拦截函数等），返回给我们当前路径下的渲染组件以及 menu 菜单的处理数据和面包屑数据。\n\n提供 `Link` 、 `useRoute` 。`Link` 为路由跳转组件，`useRoute` 可获取到当前路由下的信息。\n\n详情见[useRouter 使用](https://zhuanlan.zhihu.com/p/373920768)\n\n### 主题配置\n\n`app/theme`\n\n```js\nconst sizes = {\n  '--maxWidth': '100vw',\n  '--menuWidth': '240px',\n  '--collapseWidth': '68px',\n  '--collapseMenuWidth': '180px',\n  '--headerHeight': '68px',\n  '--footerHeight': '60px',\n  '--breadHeight': '50px',\n  '--menuItemHeight': '48px',\n};\nconst colors = {\n  '--bannerBgColor': '#37424c',\n  '--navBgColor': '#3c4752',\n  '--menuBgColor': '#37424c',\n  '--appBgColor': '#303841',\n  '--pageBgColor': '#303841',\n  '--panelBgColor': '#36404a',\n  '--appColor': '#8c98a5',\n  '--linkColor': '#9097a7',\n  '--pageLinkColor': '#8c98a5',\n  '--linkHoverColor': '#c8cddc',\n  '--linkActiveColor': '#ffffff',\n  '--borderColor': '#424e5a',\n};\n```\n\n通过 `css` 变量自行设置大小和颜色，在系统中直接使用即可。\n\n### layout 配置\n\n`layout` 可自己设计，整体框架无非就是头部导航栏和侧边菜单栏。\n\n我提供了一个管理平台 `layout` 模板，菜单通过路由返回 `menu` 数据渲染，头部导航栏可根据 `nav` 配置数据渲染，面包屑也在路由返回数据 `current` 里面。\n\n#### 通用配置\n\n`commons/layout`\n\n```js\nconst layoutConfigs = {\n  MainTop: () =\u003e {},\n  MenuBottom: () =\u003e {},\n  Link: () =\u003e {},\n  handleNavClick: () =\u003e {},\n  logo: '',\n  leftList: [],\n  rightList: [],\n  headerStyle: {},\n  menuStyle: {},\n  ...\n};\n```\n\n#### 全局 UI `i18n` 配置\n\n```jsx\nconst Index = props =\u003e (\n  \u003cUiI18n\u003e\n    \u003cLayout {...props} /\u003e\n  \u003c/UiI18n\u003e\n);\n```\n\n### i18ns 配置\n\n`app/i18ns`\n\n#### 加载\n\n`i18ns` 目录存放语言包，以语言 `key` 命名，通过 `key` 值按需加载语言包。\n\n```js\nconst getI18n = async () =\u003e {\n  const language = getLang();\n  let i18ns = await import(`@app/i18ns/${language}`);\n  i18ns = i18ns.default ?? i18ns;\n  return {i18ns, language};\n};\n```\n\n语言包配置，可以使用页面或模块命名文件，然后分别做配置，如：\n\n- zh\n  - main\n  - nav\n  - router\n  - theme\n  - login\n\n#### 具体配置\n\n```js\nconst layout = {\n  saveConfig: '保存配置',\n  copyConfig: '拷贝配置',\n  menuType: '菜单类型：',\n  vertical: '纵向',\n  horizontal: '横向',\n  compose: '横纵组合',\n  fontSize: '字体大小：',\n  layoutDesign: '布局',\n  sizeDesign: '大小',\n  colorDesign: '颜色',\n  save_cfg_msg: '主题保存成功！',\n  copy_cfg_msg: '主题拷贝成功！',\n  data_valid_msg: '请输入合法数据！',\n  data_px_msg: '请输入500-5000内数据！',\n  data_percent_msg: '请输入50-100内数据！',\n  menu_width_msg: '请输入0-300内数据！',\n};\n```\n\n#### 使用\n\n提供 3 种使用方式：\n\n**_组件_**\n\n```jsx\n\u003cIntls keys=\"login.email\"\u003e邮箱\u003c/Intls\u003e\n```\n\n通过 `key` 来获取语言数据，如果获取失败则展示 `children` 文本。\n\n**_hook_**\n\n```jsx\nconst getIntls = useIntls();\nmessage.success(getIntls('main.layout.save_cfg_msg', '成功！'));\n```\n\n`hook` 返回一个函数 `getIntls` ,第一个参数为文本 `key` 值，第二个参数是返回为空时的默认值。\n\n**_函数_**\n\n`getIntls` 函数和 `useIntls` 返回的函数一致，不同的是此函数可在任何地方使用，如纯 `js` 代码里。\n\n### 状态管理\n\n`app/store`\n\n基于 `flux` 理念，使用发布订阅模式来实现。主要提供 3 个函数：\n\n- getState：获取数据\n- setState：设置数据\n- subscribe：监听数据\n\n#### 通用方式\n\n```js\nconst {useStore} = props;\n\nconst [list, update, subscribe] = useStore('userList', {});\n```\n\n- key：userList\n- 默认值：{}\n- list：value 值\n- update：更新函数\n- subscribe：监听函数\n\n```js\nconst Page1 = props =\u003e {\n  const [list, update] = useStore('userList', []);\n  const deleteUse = async id =\u003e {\n    await fetchDel({id});\n    update();\n  };\n};\n\nconst Page2 = props =\u003e {\n  const [, , subscribe] = useStore('userList', []);\n  useEffect(() =\u003e {\n    subscribe(result =\u003e {\n      console.log(result);\n    });\n  }, []);\n};\n```\n\n#### 组合\n\n有 2 种使用方式，函数和 `hook`。当创建时使用的是同一个 `store` 时，二者数据可通用。如：\n\n```js\n// a.jsx 可检测到数据更新\nconst [state] = useStore('store-test');\n// b.jsx 设置数据\nstore.setState({'store-test', 'test data'});\n```\n\n#### 创建 `store`\n\n```js\nimport createStore from '@huxy/utils/createStore';\nimport createContainer from '@huxy/utils/createContainer';\nimport createUseContainer from '@huxy/use/createContainer';\n\nexport const container = createStore();\n\nexport const store = createContainer(container);\nexport const useStore = createUseContainer(container);\n```\n\n#### 数据名（key）\n\n```js\nexport const langName = 'lang-store';\nexport const themeName = 'theme-store';\nexport const menuTypeName = 'menuType-store';\nexport const i18nsName = 'i18ns-store';\nexport const userInfoName = 'userInfo-store';\nexport const permissionName = 'permission-store';\nexport const routersName = 'routers-store';\n```\n\n#### 创建函数（hooks）\n\n```js\nexport const langStore = store(langName);\nexport const themeStore = store(themeName);\nexport const menuTypeStore = store(menuTypeName);\nexport const i18nsStore = store(i18nsName);\nexport const userInfoStore = store(userInfoName);\nexport const permissionStore = store(permissionName);\n\nexport const useLangStore = initState =\u003e useStore(langName, initState);\nexport const useThemeStore = initState =\u003e useStore(themeName, initState);\nexport const useMenuTypeStore = initState =\u003e useStore(menuTypeName, initState);\nexport const useI18nsStore = initState =\u003e useStore(i18nsName, initState);\nexport const useUserInfoStore = initState =\u003e useStore(userInfoName, initState);\nexport const usePermissionStore = initState =\u003e useStore(permissionName, initState);\n```\n\n#### 使用\n\n```jsx\n// useI18nsStore\nconst Intls = ({keys, children}) =\u003e {\n  const [i18ns] = useI18nsStore();\n  return (keys \u0026\u0026 i18ns?.getValue(keys)) ?? children ?? '';\n};\n// i18nsStore\nexport const getIntls = (keys, def) =\u003e (keys \u0026\u0026 i18nsStore.getState()?.getValue(keys)) ?? def ?? '';\n```\n\n### utils、components、hooks\n\n组件可分为基础组件、业务组件，基础组件粒度低、通用性高，业务组件包含了一些业务场景，在某一领域模型内通用，通常是基础组件组合而来。\n\n#### utils\n\n例如：格式化树\n\n```js\n// formatTree\nconst fixIcon = router =\u003e\n  router.map(item =\u003e {\n    item.key = item.key || item.path;\n    item.icon = \u003cIcon icon={item.iconKey || 'EyeInvisibleOutlined'} /\u003e;\n    return item;\n  });\n\nconst formatTree = arr =\u003e\n  traverItem(item =\u003e {\n    if (!isValidArr(item.children)) {\n      item.isLeaf = true;\n    }\n  })(arr2TreeByPath(fixIcon(arr)));\n\nexport default formatTree;\n```\n\n详情见 [utils](https://github.com/ahyiru/utils)\n\n#### components\n\n例如：超出宽度文本自动省略并展示 `tooltip`\n\n```jsx\nconst style = {\n  overflow: 'hidden',\n  textOverflow: 'ellipsis',\n  whiteSpace: 'nowrap',\n  display: 'inline-block',\n  width: '100%',\n};\n\nconst Ellipsis = ({children, ttProps}) =\u003e {\n  const span = useRef();\n  const [ellipsis, setEllipsis] = useState(false);\n  const state = useEleResize(span, 250);\n  useEffect(() =\u003e {\n    if (span.current) {\n      const {width: tWidth} = getTextSize(children);\n      const {width} = getPosition(span.current);\n      const isEllipsis = tWidth \u003e width;\n      if (isEllipsis !== ellipsis) {\n        setEllipsis(isEllipsis);\n      }\n    }\n  }, [children, state.width]);\n  return (\n    \u003cspan ref={span} style={style}\u003e\n      {ellipsis ? (\n        \u003cTooltip placement=\"topLeft\" title={children} {...ttProps}\u003e\n          {children}\n        \u003c/Tooltip\u003e\n      ) : (\n        \u003cspan\u003e{children}\u003c/span\u003e\n      )}\n    \u003c/span\u003e\n  );\n};\n```\n\n**_Input_**\n\n```jsx\nconst Input = ({className, ...rest}) =\u003e {\n  const cls = ['h-input', ...(className?.split(' ') ?? [])]\n    .filter(Boolean)\n    .map(c =\u003e styles[c])\n    .join(' ');\n  return \u003cinput className={cls} {...rest} /\u003e;\n};\n\nexport default Input;\n```\n\n**_Radio_**\n\n```jsx\nconst Radio = ({options, name, value: checked, onChange, ...rest}) =\u003e (\n  \u003cdiv className=\"radio-wrap\" {...rest} style={{display: 'flex'}}\u003e\n    {options.map(({value, label}) =\u003e (\n      \u003cdiv key={value} className=\"radio-item\" onClick={e =\u003e onChange?.(value, e)} style={{display: 'flex', alignItems: 'center', marginRight: '12px', cursor: 'pointer'}}\u003e\n        \u003cinput type=\"radio\" name={name} value={value} checked={value === checked} readOnly /\u003e\n        \u003cspan style={{marginLeft: '6px'}}\u003e{value}\u003c/span\u003e\n      \u003c/div\u003e\n    ))}\n  \u003c/div\u003e\n);\n\nexport default Radio;\n```\n\n**_Tooltip_**\n\n```jsx\nconst Tooltip = ({children, title, placement}) =\u003e (\n  \u003cspan className={`tooltip-${placement}`} tooltips={title}\u003e\n    {children}\n  \u003c/span\u003e\n);\n\nexport default Tooltip;\n```\n\n**_Drawer_**\n\n```jsx\nconst Drawer = ({visible, onClose, footer, header, className, children, width = '300px'}) =\u003e {\n  const cls = ['drawer-wrap', visible ? 'open' : '', ...(className?.split(' ') ?? [])]\n    .filter(Boolean)\n    .map(c =\u003e styles[c])\n    .join(' ');\n  return (\n    \u003cMask open={visible} close={onClose} delay={250} hasMask={true} className=\"huxy-drawer\"\u003e\n      \u003cdiv className={cls} style={{width}}\u003e\n        \u003cdiv className={styles['drawer-container']}\u003e\n          \u003cdiv className={styles['drawer-header']}\u003e\n            \u003ca className={styles['ico-close']} onClick={onClose} /\u003e\n            {header}\n          \u003c/div\u003e\n          \u003cdiv className={styles['drawer-content']}\u003e{children}\u003c/div\u003e\n          {footer ? \u003cdiv className={styles['drawer-footer']}\u003e{footer}\u003c/div\u003e : null}\n        \u003c/div\u003e\n      \u003c/div\u003e\n    \u003c/Mask\u003e\n  );\n};\n\nexport default Drawer;\n```\n\n详情见 [components](https://github.com/ahyiru/components)\n\n#### hooks\n\n例如：请求列表数据 `hook` ，包含返回结果处理、分页、搜索、更新等。\n\nuseFetchList：\n\n```jsx\nconst useFetchList = (fetchList, initParams = null, handleResult, commonParams = null) =\u003e {\n  const [result, updateResult] = useAsync({});\n  const update = useCallback(params =\u003e updateResult({res: fetchList({...commonParams, ...params})}, handleResult), []);\n  useEffect(() =\u003e {\n    update({...initParams});\n  }, []);\n  const {res} = result;\n  const isPending = !res || res.pending;\n\n  return [{isPending, data: res?.result}, update];\n};\n```\n\nuseHandleList：\n\n```jsx\nconst useHandleList = (fetchList, initParams = null, handleResult, commonParams = null) =\u003e {\n  const {current, size, ...rest} = initParams || {};\n  const search = useRef(rest || {});\n  const page = useRef({current: current || 1, size: size || 10});\n  const [result, update] = useFetchList(fetchList, {...page.current, ...search.current}, handleResult, commonParams);\n\n  const pageChange = (current, size) =\u003e {\n    page.current = {current, size};\n    update({\n      ...page.current,\n      ...search.current,\n    });\n  };\n  const searchList = values =\u003e {\n    search.current = values;\n    page.current = {...page.current, current: 1};\n    update({...page.current, ...search.current});\n  };\n  const handleUpdate = params =\u003e {\n    const {current, size, ...rest} = params;\n    page.current = {current: current ?? page.current.current, size: size ?? page.current.size};\n    search.current = {...search.current, ...rest};\n    update({...page.current, ...search.current});\n  };\n\n  return [result, handleUpdate, pageChange, searchList];\n};\n```\n\n详情见 [use](https://github.com/ahyiru/use)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fahyiru%2Fhuxy-admin","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fahyiru%2Fhuxy-admin","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fahyiru%2Fhuxy-admin/lists"}