Huxy Admin is a customizable admin dashboard template based on React19. 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 应用。
- 主题配置 ✅
- i18n配置 ✅
- 路由配置 ✅
- 权限管理 ✅
- API管理 ✅
- 项目管理 ✅
- 角色管理 ✅
- 页面监控 ✅
- 页面日志 ✅
- 会员管理 ✅
- 订单管理 ✅
- 消息管理 ✅
- 邮件管理 ✅
- 页面管理 ✅
- 文件管理 ✅
- 文档管理 ✅
- 需求管理 ✅
可自行配置运行项目,如运行 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
:代码规范检查,包含eslint
和stylelint
npm run lint-fix
:代码规范修正npm run prettier
:代码美化npm run release
:自动打tag
并生成CHANGELOG
提供了 git
代码提交钩子 husky
,git
提交信息规范 commitlint
,支持 pnpm
安装依赖。
主要有 运行端口、资源路径、打包路径、basepath、代理、mock 等。
configs/app.js
const app = {
HOST: process.env.IP || 'https://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: 'https://api.ihuxy.com', // 服务代理
prefix: '/api', // 代理前缀
},
MOCK: 'https://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,
},
];
app/apis
可自行配置路径 prefix
、headers
、token
、credentials
等。
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,
},
],
};
const output = useRouter(input);
通过传入一些配置(主要是路由类型、路由列表、路由拦截函数等),返回给我们当前路径下的渲染组件以及 menu 菜单的处理数据和面包屑数据。
提供 Link
、 useRoute
。Link
为路由跳转组件,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
模板,菜单通过路由返回 menu
数据渲染,头部导航栏可根据 nav
配置数据渲染,面包屑也在路由返回数据 current
里面。
commons/layout
const layoutConfigs = {
MainTop: () => {},
MenuBottom: () => {},
Link: () => {},
handleNavClick: () => {},
logo: '',
leftList: [],
rightList: [],
headerStyle: {},
menuStyle: {},
...
};
const Index = props => (
<UiI18n>
<Layout {...props} />
</UiI18n>
);
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'});
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);
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';
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 ?? '';
组件可分为基础组件、业务组件,基础组件粒度低、通用性高,业务组件包含了一些业务场景,在某一领域模型内通用,通常是基础组件组合而来。
例如:格式化树
// 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
例如:超出宽度文本自动省略并展示 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
例如:请求列表数据 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