Hair Keeper v1.0.0:一个高度集成、深度定制、约定优于配置的全栈Web应用模板,旨在保持灵活性的同时提供一套基于成熟架构的开发底座,自带身份认证、权限控制、丰富前端组件、文件上传、后台任务、智能体开发等丰富功能,提供AI开发辅助,免于纠结功能如何实现,可快速上手专注于业务逻辑
This commit is contained in:
38
src/constants/data-table.ts
Normal file
38
src/constants/data-table.ts
Normal 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];
|
||||
75
src/constants/menu-icons.ts
Normal file
75
src/constants/menu-icons.ts
Normal 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
326
src/constants/menu.ts
Normal 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",
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
],
|
||||
}] : [])
|
||||
]
|
||||
58
src/constants/permissions.ts
Normal file
58
src/constants/permissions.ts
Normal 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
6
src/constants/site.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* 站点配置常量
|
||||
*/
|
||||
export const SITE_NAME = 'Hair Keeper'
|
||||
export const SITE_DESCRIPTION = '高度集成、深度定制、约定优于配置的全栈Web应用模板,旨在保持灵活性的同时提供一套基于成熟架构的开发底座,自带身份认证、权限控制、丰富前端组件、文件上传、后台任务、智能体开发等丰富功能,提供AI开发辅助,免于纠结功能如何实现,可快速上手专注于业务逻辑'
|
||||
export const SITE_VERSION = 'v1.0.0'
|
||||
Reference in New Issue
Block a user