Initial commit: One Pipe System

完整的管理系统,包含账户管理、卡片管理、套餐管理、财务管理等功能模块。

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
sexygoat
2026-01-22 16:35:33 +08:00
commit 222e5bb11a
495 changed files with 145440 additions and 0 deletions

View File

@@ -0,0 +1,73 @@
import { AppRouteRecord } from '@/types/router'
/**
* 将菜单数据转换为路由配置
* @param route 菜单数据对象
* @param parentPath 父级路径
* @returns 处理后的路由配置
*/
export const menuDataToRouter = (route: AppRouteRecord, parentPath = ''): AppRouteRecord => {
const { id, name, component, meta, children } = route
const fullPath = buildRoutePath(route, parentPath)
return {
id,
name,
path: fullPath,
component,
meta,
children: processChildren(children || [], fullPath)
}
}
/**
* 构建路由完整路径
* @param route 菜单数据对象
* @param parentPath 父级路径
* @returns 构建后的完整路径
*/
const buildRoutePath = (route: AppRouteRecord, parentPath: string): string => {
if (!route.path) return ''
// iframe 类型路由直接使用原始路径
if (route.meta?.isIframe) return route.path
// 拼接并规范化路径
return parentPath ? `${parentPath}/${route.path}`.replace(/\/+/g, '/') : route.path
}
/**
* 处理子路由
* @param children 子路由数组
* @param parentPath 父级路径
* @returns 处理后的子路由数组
*/
const processChildren = (children: AppRouteRecord[], parentPath: string): AppRouteRecord[] => {
if (!Array.isArray(children) || children.length === 0) return []
return children.map((child) => menuDataToRouter(child, parentPath))
}
/**
* 保存 iframe 路由到 sessionStorage 中
* @param list iframe 路由列表
*/
export const saveIframeRoutes = (list: AppRouteRecord[]): void => {
if (list.length > 0) {
sessionStorage.setItem('iframeRoutes', JSON.stringify(list))
}
}
/**
* 获取 iframe 路由
* @returns iframe 路由列表
*/
export const getIframeRoutes = (): AppRouteRecord[] => {
try {
return JSON.parse(sessionStorage.getItem('iframeRoutes') || '[]')
} catch (error) {
console.error('解析 iframe 路由失败:', error)
return []
}
}

View File

@@ -0,0 +1,251 @@
/**
* 动态路由处理
* 根据接口返回的菜单列表注册动态路由
*/
import type { Router, RouteRecordRaw } from 'vue-router'
import type { AppRouteRecord } from '@/types/router'
import { saveIframeRoutes } from './menuToRouter'
import { RoutesAlias } from '../routesAlias'
import { h } from 'vue'
/**
* 动态导入 views 目录下所有 .vue 组件
*/
const modules: Record<string, () => Promise<any>> = import.meta.glob('../../views/**/*.vue')
/**
* 注册异步路由
* 将接口返回的菜单列表转换为 Vue Router 路由配置,并添加到传入的 router 实例中
* @param router Vue Router 实例
* @param menuList 接口返回的菜单列表
*/
export function registerDynamicRoutes(router: Router, menuList: AppRouteRecord[]): void {
// 用于局部收集 iframe 类型路由
const iframeRoutes: AppRouteRecord[] = []
// 检测菜单列表中是否有重复路由
checkDuplicateRoutes(menuList)
// 遍历菜单列表,注册路由
menuList.forEach((route) => {
// 只有还没注册过的路由才进行注册
if (route.name && !router.hasRoute(route.name)) {
const routeConfig = convertRouteComponent(route, iframeRoutes)
router.addRoute(routeConfig as RouteRecordRaw)
}
})
// 保存 iframe 路由
saveIframeRoutes(iframeRoutes)
}
/**
* 路径解析函数:处理父路径和子路径的拼接
*/
function resolvePath(parent: string, child: string): string {
return [parent.replace(/\/$/, ''), child.replace(/^\//, '')].filter(Boolean).join('/')
}
/**
* 检测菜单中的重复路由(包括子路由)
*/
function checkDuplicateRoutes(routes: AppRouteRecord[], parentPath = ''): void {
// 用于检测动态路由中的重复项
const routeNameMap = new Map<string, string>() // 路由名称 -> 路径
const componentPathMap = new Map<string, string>() // 组件路径 -> 路由信息
const checkRoutes = (routes: AppRouteRecord[], parentPath = '') => {
routes.forEach((route) => {
// 处理路径拼接
const currentPath = route.path || ''
const fullPath = resolvePath(parentPath, currentPath)
// 名称重复检测
if (route.name) {
if (routeNameMap.has(String(route.name))) {
console.warn(`[路由警告] 名称重复: "${String(route.name)}"`)
} else {
routeNameMap.set(String(route.name), fullPath)
}
}
// 组件路径重复检测
if (route.component) {
const componentPath = getComponentPathString(route.component)
if (componentPath && componentPath !== RoutesAlias.Home) {
const componentKey = `${parentPath}:${componentPath}`
if (componentPathMap.has(componentKey)) {
console.warn(`[路由警告] 路径重复: "${componentPath}"`)
} else {
componentPathMap.set(componentKey, fullPath)
}
}
}
// 递归处理子路由
if (route.children?.length) {
checkRoutes(route.children, fullPath)
}
})
}
checkRoutes(routes, parentPath)
}
/**
* 获取组件路径的字符串表示
*/
function getComponentPathString(component: any): string {
if (typeof component === 'string') {
return component
}
// 对于其他别名路由,获取组件名称
for (const key in RoutesAlias) {
if (RoutesAlias[key as keyof typeof RoutesAlias] === component) {
return `RoutesAlias.${key}`
}
}
return ''
}
/**
* 根据组件路径动态加载组件
* @param componentPath 组件路径(不包含 ../../views 前缀和 .vue 后缀)
* @param routeName 当前路由名称(用于错误提示)
* @returns 组件加载函数
*/
function loadComponent(componentPath: string, routeName: string): () => Promise<any> {
// 如果路径为空,直接返回一个空的组件
if (componentPath === '') {
return () =>
Promise.resolve({
render() {
return h('div', {})
}
})
}
// 构建可能的路径
const fullPath = `../../views${componentPath}.vue`
const fullPathWithIndex = `../../views${componentPath}/index.vue`
// 先尝试直接路径,再尝试添加/index的路径
const module = modules[fullPath] || modules[fullPathWithIndex]
if (!module) {
console.error(
`[路由错误] 未找到组件:${routeName},尝试过的路径: ${fullPath}${fullPathWithIndex}`
)
return () =>
Promise.resolve({
render() {
return h('div', `组件未找到: ${routeName}`)
}
})
}
return module
}
/**
* 转换后的路由配置类型
*/
interface ConvertedRoute extends Omit<RouteRecordRaw, 'children'> {
id?: number
children?: ConvertedRoute[]
component?: RouteRecordRaw['component'] | (() => Promise<any>)
}
/**
* 转换路由组件配置
*/
function convertRouteComponent(
route: AppRouteRecord,
iframeRoutes: AppRouteRecord[],
depth = 0
): ConvertedRoute {
const { component, children, ...routeConfig } = route
// 基础路由配置
const converted: ConvertedRoute = {
...routeConfig,
component: undefined
}
// 是否为一级菜单
const isFirstLevel = depth === 0 && route.children?.length === 0
if (route.meta.isIframe) {
handleIframeRoute(converted, route, iframeRoutes)
} else if (isFirstLevel) {
handleLayoutRoute(converted, route, component as string)
} else {
handleNormalRoute(converted, component as string, String(route.name))
}
// 递归时增加深度
if (children?.length) {
converted.children = children.map((child) =>
convertRouteComponent(child, iframeRoutes, depth + 1)
)
}
return converted
}
/**
* 处理 iframe 类型路由
*/
function handleIframeRoute(
converted: ConvertedRoute,
route: AppRouteRecord,
iframeRoutes: AppRouteRecord[]
): void {
converted.path = `/outside/iframe/${String(route.name)}`
converted.component = () => import('@/views/outside/Iframe.vue')
iframeRoutes.push(route)
}
/**
* 处理一级菜单路由
*/
function handleLayoutRoute(
converted: ConvertedRoute,
route: AppRouteRecord,
component: string | undefined
): void {
converted.component = () => import('@/views/index/index.vue')
converted.path = `/${(route.path?.split('/')[1] || '').trim()}`
converted.name = ''
route.meta.isFirstLevel = true
converted.children = [
{
id: route.id,
path: route.path,
name: route.name,
component: loadComponent(component as string, String(route.name)),
meta: route.meta
}
]
}
/**
* 处理普通路由
*/
function handleNormalRoute(
converted: ConvertedRoute,
component: string | undefined,
routeName: string
): void {
if (component) {
const aliasComponent = RoutesAlias[
component as keyof typeof RoutesAlias
] as unknown as RouteRecordRaw['component']
converted.component = aliasComponent || loadComponent(component as string, routeName)
}
}

58
src/router/utils/utils.ts Normal file
View File

@@ -0,0 +1,58 @@
import { useTheme } from '@/composables/useTheme'
import { useSettingStore } from '@/store/modules/setting'
import { RouteLocationNormalized, RouteRecordRaw } from 'vue-router'
import AppConfig from '@/config'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
import { $t } from '@/locales'
/** 扩展的路由配置类型 */
export type AppRouteRecordRaw = RouteRecordRaw & {
hidden?: boolean
}
/** 顶部进度条配置 */
export const configureNProgress = () => {
NProgress.configure({
easing: 'ease',
speed: 600,
showSpinner: false,
trickleSpeed: 200,
parent: 'body'
})
}
/**
* 设置页面标题,根据路由元信息和系统信息拼接标题
* @param to 当前路由对象
*/
export const setPageTitle = (to: RouteLocationNormalized): void => {
const { title } = to.meta
if (title) {
setTimeout(() => {
document.title = `${formatMenuTitle(String(title))} - ${AppConfig.systemInfo.name}`
}, 150)
}
}
/**
* 根据路由元信息设置系统主题
* @param to 当前路由对象
*/
export const setSystemTheme = (to: RouteLocationNormalized): void => {
if (to.meta.setTheme) {
useTheme().switchThemeStyles(useSettingStore().systemThemeType)
}
}
/**
* 格式化菜单标题
* @param title 菜单标题,可以是 i18n 的 key也可以是字符串
* @returns 格式化后的菜单标题
*/
export const formatMenuTitle = (title: string): string => {
if (title) {
return title.startsWith('menus.') ? $t(title) : title
}
return ''
}