Hair Keeper v1.0.0:一个高度集成、深度定制、约定优于配置的全栈Web应用模板,旨在保持灵活性的同时提供一套基于成熟架构的开发底座,自带身份认证、权限控制、丰富前端组件、文件上传、后台任务、智能体开发等丰富功能,提供AI开发辅助,免于纠结功能如何实现,可快速上手专注于业务逻辑

This commit is contained in:
2025-11-13 15:24:54 +08:00
commit 42be39b343
249 changed files with 38843 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
export type DataTableConfig = typeof dataTableConfig;
export const dataTableConfig = {
sortOrders: [
{ label: "升序", value: "asc" as const },
{ label: "降序", value: "desc" as const },
],
filterVariants: [
"text",
"number",
"range",
"date",
"dateRange",
"boolean",
"select",
"multiSelect",
] as const,
operators: [
"iLike",
"notILike",
"eq",
"ne",
"inArray",
"notInArray",
"isEmpty",
"isNotEmpty",
"lt",
"lte",
"gt",
"gte",
"isBetween",
"isRelativeToToday",
] as const,
joinOperators: ["and", "or"] as const,
};
export type FilterVariant = DataTableConfig["filterVariants"][number];

View File

@@ -0,0 +1,75 @@
import {
Home,
History,
BarChart3,
Settings,
UserCog,
Layout,
Sofa,
Code2,
FolderKanban,
FolderTree,
Rocket,
Server,
Boxes,
Folder,
Blocks,
FileText,
HardDriveUpload,
List,
Network,
ArrowLeftRight,
Package,
ShoppingCart,
Store,
ShoppingBag,
ClipboardCheck,
FileBarChart,
Globe,
Menu,
LucideIcon
} from 'lucide-react'
/**
* 菜单图标映射表
* 将字符串图标名称映射到实际的 Lucide 图标组件
*/
export const menuIconMap: Record<string, LucideIcon> = {
'Home': Home,
'History': History,
'BarChart3': BarChart3,
'Settings': Settings,
'UserCog': UserCog,
'Layout': Layout,
'Sofa': Sofa,
'Code2': Code2,
'FolderKanban': FolderKanban,
'FolderTree': FolderTree,
'Rocket': Rocket,
'Server': Server,
'Boxes': Boxes,
'Folder': Folder,
'Blocks': Blocks,
'FileText': FileText,
'HardDriveUpload': HardDriveUpload,
'List': List,
'Network': Network,
'ArrowLeftRight': ArrowLeftRight,
'Package': Package,
'ShoppingCart': ShoppingCart,
'Store': Store,
'ShoppingBag': ShoppingBag,
'ClipboardCheck': ClipboardCheck,
'FileBarChart': FileBarChart,
'Globe': Globe,
}
/**
* 根据图标名称获取对应的 Lucide 图标组件
* @param iconName 图标名称
* @returns Lucide 图标组件,如果找不到则返回通用菜单图标
*/
export function getMenuIcon(iconName?: string): LucideIcon {
if (!iconName) return Menu
return menuIconMap[iconName] || Menu
}

326
src/constants/menu.ts Normal file
View File

@@ -0,0 +1,326 @@
import { Permissions, evaluatePermissionExpression } from './permissions'
import { SITE_NAME } from './site'
export interface MenuItem {
title: string
href?: string
icon?: string
children?: MenuItem[]
badge?: number
permission?: string
}
/**
* 获取指定路径的子菜单列表
* @param href 父菜单的 href
* @returns 子菜单列表,如果不存在则返回空数组
*/
export function getSubMenus(href: string): MenuItem[] {
// 递归查找菜单项
const findMenuItem = (items: MenuItem[], targetHref: string): MenuItem | undefined => {
for (const item of items) {
if (item.href === targetHref) {
return item
}
if (item.children) {
const found = findMenuItem(item.children, targetHref)
if (found) return found
}
}
return undefined
}
const menuItem = findMenuItem(menuItems, href)
return menuItem?.children || []
}
/**
* 获取指定路径的默认子菜单 href
* @param parentHref 父菜单的 href
* @returns 默认的子菜单 href如果不存在则返回 undefined
*/
export function getDefaultSubMenuHref(parentHref: string): string | undefined {
const subMenus = getSubMenus(parentHref)
return subMenus.length > 0 ? subMenus[0].href : undefined
}
/**
* 从路径中提取当前激活的子菜单值
* @param pathname 当前路径
* @param parentHref 父菜单的 href
* @returns 当前激活的子菜单路径段(最后一段)
*/
export function getActiveSubMenuValue(pathname: string, parentHref: string): string {
const subMenus = getSubMenus(parentHref)
// 从路径中提取最后一段作为 value
const segments = pathname.split('/').filter(Boolean)
const lastSegment = segments[segments.length - 1]
// 检查是否是有效的子菜单
const isValid = subMenus.some(menu => menu.href?.endsWith(`/${lastSegment}`))
// 如果有效返回最后一段,否则返回默认值(第一个子菜单的最后一段)
if (isValid) {
return lastSegment
}
const defaultHref = getDefaultSubMenuHref(parentHref)
if (defaultHref) {
const defaultSegments = defaultHref.split('/').filter(Boolean)
return defaultSegments[defaultSegments.length - 1]
}
return ''
}
/**
* 获取指定路径的菜单权限要求
* @param pathname 当前路径
* @returns 菜单项的权限要求,如果找不到则返回 undefined
*/
export function getMenuPermission(pathname: string): string | undefined {
// 递归查找菜单项
const findMenuPermission = (items: MenuItem[], targetPath: string): string | undefined => {
for (const item of items) {
// 精确匹配
if (item.href === targetPath) {
return item.permission
}
// 如果当前路径以菜单项的 href 开头(用于匹配子路由)
if (item.href && targetPath.startsWith(item.href) && item.href !== '/') {
// 如果有子菜单,继续在子菜单中查找
if (item.children) {
const childPermission = findMenuPermission(item.children, targetPath)
if (childPermission !== undefined) return childPermission
}
// 如果没有子菜单或子菜单中没找到,返回当前菜单项权限
return item.permission
}
// 检查子菜单
if (item.children) {
const childPermission = findMenuPermission(item.children, targetPath)
if (childPermission !== undefined) return childPermission
}
}
return undefined
}
return findMenuPermission(menuItems, pathname)
}
/**
* 根据用户权限过滤菜单项
* @param items 菜单项列表
* @param userPermissions 用户权限列表
* @param isSuperAdmin 是否为超级管理员
* @returns 过滤后的菜单项列表
*/
export function filterMenuItemsByPermission(
items: MenuItem[],
userPermissions: string[],
isSuperAdmin: boolean
): MenuItem[] {
// 权限检查函数
const hasPermission = (permission?: string): boolean => {
if (!permission) return true
if (isSuperAdmin) return true
return evaluatePermissionExpression(permission, userPermissions)
}
return items
.map((item) => {
if (item.children) {
const visibleChildren = item.children
.map((child) => {
// 递归处理子菜单的子菜单
if (child.children) {
const visibleGrandChildren = child.children.filter((grandChild) =>
hasPermission(grandChild.permission)
)
return visibleGrandChildren.length > 0
? { ...child, children: visibleGrandChildren }
: null
}
return hasPermission(child.permission) ? child : null
})
.filter(Boolean) as MenuItem[]
return visibleChildren.length > 0
? { ...item, children: visibleChildren }
: null
}
// 特殊处理首页和设置页面
if (item.href === '/') return item
if (item.href === '/settings') return isSuperAdmin ? item : null
return hasPermission(item.permission) ? item : null
})
.filter(Boolean) as MenuItem[]
}
/**
* 根据路径查找菜单项标题
* @param pathname 当前路径
* @param maxDepth 最大匹配深度1表示只匹配一级菜单2表示匹配到二级菜单以此类推。undefined表示不限制深度
* @returns 菜单项标题,如果找不到则返回站点名称
*/
export function getMenuTitle(pathname: string, maxDepth?: number): string {
// 递归查找菜单项
const findMenuTitle = (items: MenuItem[], targetPath: string, currentDepth: number = 1): string | undefined => {
for (const item of items) {
// 精确匹配
if (item.href === targetPath) {
return item.title
}
// 如果当前路径以菜单项的 href 开头(用于匹配子路由)
if (item.href && targetPath.startsWith(item.href) && item.href !== '/') {
// 如果达到最大深度限制,返回当前菜单项标题
if (maxDepth !== undefined && currentDepth >= maxDepth) {
return item.title
}
// 如果有子菜单,继续在子菜单中查找
if (item.children) {
const childTitle = findMenuTitle(item.children, targetPath, currentDepth + 1)
if (childTitle) return childTitle
}
// 如果没有子菜单或子菜单中没找到,返回当前菜单项标题
return item.title
}
// 检查子菜单(仅当未达到深度限制时)
if (item.children && (maxDepth === undefined || currentDepth < maxDepth)) {
const childTitle = findMenuTitle(item.children, targetPath, currentDepth + 1)
if (childTitle) return childTitle
}
}
return undefined
}
const title = findMenuTitle(menuItems, pathname)
return title || SITE_NAME
}
export const menuItems: MenuItem[] = [
{
title: '首页',
href: '/',
icon: 'Home',
permission: '',
},
{
title: '用户管理',
href: '/users',
icon: 'UserCog',
permission: Permissions.USER_MANAGE,
},
{
title: '系统设置',
href: '/settings',
icon: 'Settings',
permission: 'SUPER_ADMIN_ONLY', // only super admin
},
...(process.env.NODE_ENV === 'development' ? [{
title: '开发',
href: '/dev',
icon: 'Code2',
permission: "SUPER_ADMIN_ONLY",
children: [
{
title: '项目文件',
href: '/dev/file',
icon: 'FolderKanban',
permission: "SUPER_ADMIN_ONLY",
children: [
{
title: '文件列表',
href: '/dev/file/list',
icon: 'List',
permission: "SUPER_ADMIN_ONLY",
},
{
title: '目录结构',
href: '/dev/file/directory-tree',
icon: 'Folder',
permission: "SUPER_ADMIN_ONLY",
},
{
title: '依赖关系图',
href: '/dev/file/graph',
icon: 'Network',
permission: "SUPER_ADMIN_ONLY",
},
],
},
{
title: '项目结构',
href: '/dev/arch',
icon: 'FolderTree',
permission: "SUPER_ADMIN_ONLY",
children: [
{
title: '依赖包',
href: '/dev/arch/package',
icon: 'Boxes',
permission: "SUPER_ADMIN_ONLY",
},
],
},
{
title: '前端组件设计',
href: '/dev/frontend-design',
icon: 'Layout',
permission: "SUPER_ADMIN_ONLY",
children: [
{
title: 'UI组件',
href: '/dev/frontend-design/ui',
icon: 'Blocks',
permission: "SUPER_ADMIN_ONLY",
},
// 仍在开发中,先注释掉
// {
// title: '页面',
// href: '/dev/frontend-design/page',
// icon: 'FileText',
// permission: "SUPER_ADMIN_ONLY",
// },
],
},
// 仍在开发中,先注释掉
// {
// title: '后端服务设计',
// href: '/dev/backend-design',
// icon: 'Server',
// permission: "SUPER_ADMIN_ONLY",
// },
// {
// title: '运行发布',
// href: '/dev/run',
// icon: 'Rocket',
// permission: "SUPER_ADMIN_ONLY",
// children: [
// {
// title: '容器',
// href: '/dev/run/container',
// icon: 'Blocks',
// permission: "SUPER_ADMIN_ONLY",
// },
// {
// title: '部署',
// href: '/dev/run/deploy',
// icon: 'HardDriveUpload',
// permission: "SUPER_ADMIN_ONLY",
// },
// ],
// },
],
}] : [])
]

View File

@@ -0,0 +1,58 @@
// 权限常量列表
// 每个权限标识一个功能点,用于前后端一致的权限控制
export const Permissions = {
USER_MANAGE: '用户管理',
// SUPER_ADMIN_ONLY: '超级管理员' // 该权限不能被授权,因此注释掉,但在项目中统一用"SUPER_ADMIN_ONLY"表示超级管理员权限
} as const;
export type Permission = typeof Permissions[keyof typeof Permissions];
export const ALL_PERMISSIONS: Permission[] = Object.values(Permissions);
// 判断用户权限列表是否满足表达式,如 "A&B|(C&D)"
export function evaluatePermissionExpression(
expr: string,
userPermissions: string[]
): boolean {
if (!expr || expr.trim() === '') { // expr为空、null或者undefined都返回true
return true;
}
// 分词
const tokens: string[] = []
const regex = /(\&|\||\(|\))/
expr.split(regex).map(s => s.trim()).filter(s => s).forEach(tok => tokens.push(tok))
let pos = 0
function parseExpr(): boolean {
let value = parseTerm()
while (tokens[pos] === '|') {
pos++
value = value || parseTerm()
}
return value
}
function parseTerm(): boolean {
let value = parseFactor()
while (tokens[pos] === '&') {
pos++
value = value && parseFactor()
}
return value
}
function parseFactor(): boolean {
const tok = tokens[pos++]
if (tok === '(') {
const value = parseExpr()
if (tokens[pos] === ')') pos++
return value
}
// 普通权限名
return userPermissions.includes(tok)
}
try {
return parseExpr()
} catch {
return false
}
}

6
src/constants/site.ts Normal file
View File

@@ -0,0 +1,6 @@
/**
* 站点配置常量
*/
export const SITE_NAME = 'Hair Keeper'
export const SITE_DESCRIPTION = '高度集成、深度定制、约定优于配置的全栈Web应用模板旨在保持灵活性的同时提供一套基于成熟架构的开发底座自带身份认证、权限控制、丰富前端组件、文件上传、后台任务、智能体开发等丰富功能提供AI开发辅助免于纠结功能如何实现可快速上手专注于业务逻辑'
export const SITE_VERSION = 'v1.0.0'