diff --git a/package-lock.json b/package-lock.json
index 4c99dc6..abcd512 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,6 +14,7 @@
"element-plus": "^2.6.3",
"js-cookie": "^3.0.5",
"mermaid": "^11.12.2",
+ "path-browserify": "^1.0.1",
"pinia": "^2.1.7",
"vue": "^3.4.21",
"vue-router": "^4.3.0",
@@ -3390,6 +3391,12 @@
"integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==",
"license": "MIT"
},
+ "node_modules/path-browserify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz",
+ "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
+ "license": "MIT"
+ },
"node_modules/path-data-parser": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz",
diff --git a/package.json b/package.json
index 32dd173..38d3251 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,7 @@
"element-plus": "^2.6.3",
"js-cookie": "^3.0.5",
"mermaid": "^11.12.2",
+ "path-browserify": "^1.0.1",
"pinia": "^2.1.7",
"vue": "^3.4.21",
"vue-router": "^4.3.0",
diff --git a/src/assets/404_images/404.png b/src/assets/404_images/404.png
new file mode 100644
index 0000000..3d8e230
Binary files /dev/null and b/src/assets/404_images/404.png differ
diff --git a/src/assets/404_images/404_cloud.png b/src/assets/404_images/404_cloud.png
new file mode 100644
index 0000000..c6281d0
Binary files /dev/null and b/src/assets/404_images/404_cloud.png differ
diff --git a/src/layout/MainLayout.vue b/src/layout/MainLayout.vue
index 7b326cb..5e7b666 100644
--- a/src/layout/MainLayout.vue
+++ b/src/layout/MainLayout.vue
@@ -24,121 +24,7 @@
-
+
@@ -151,35 +37,20 @@
@@ -380,81 +218,10 @@ onMounted(async () => {
overflow: hidden;
}
-/* 侧边栏 */
-.sidebar {
- width: 260px;
- background: white;
- box-shadow: 2px 0 10px rgba(0, 0, 0, 0.05);
- border-right: 1px solid #f5f7fa;
- display: flex;
- flex-direction: column;
- transition: width 0.3s;
- height: 100%;
- overflow: hidden;
-}
-
-.sidebar.collapse {
- width: 64px;
-}
-
-.nav-menu {
- flex: 1;
- border-right: none;
- overflow-y: auto;
- overflow-x: hidden;
-}
-
-/* 自定义滚动条样式 */
-.nav-menu::-webkit-scrollbar {
- width: 6px;
-}
-
-.nav-menu::-webkit-scrollbar-track {
- background: #f5f5f5;
- border-radius: 3px;
-}
-
-.nav-menu::-webkit-scrollbar-thumb {
- background: #c0c4cc;
- border-radius: 3px;
-}
-
-.nav-menu::-webkit-scrollbar-thumb:hover {
- background: #a0a4ac;
-}
-
-.sidebar-toggle {
- height: 48px;
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
- border-top: 1px solid #e4e7ed;
- transition: background-color 0.3s;
-}
-
-.sidebar-toggle:hover {
- background-color: #f5f7fa;
-}
-
/* 主内容 */
.main-content {
flex: 1;
overflow: auto;
background: #f5f7fa;
}
-
-/* 响应式 */
-@media (max-width: 768px) {
- .sidebar {
- position: fixed;
- left: -260px;
- z-index: 1000;
- height: calc(100vh - 60px);
- }
-
- .sidebar:not(.collapse) {
- left: 0;
- }
-}
diff --git a/src/layout/components/Sidebar/Item.vue b/src/layout/components/Sidebar/Item.vue
new file mode 100644
index 0000000..29d3eae
--- /dev/null
+++ b/src/layout/components/Sidebar/Item.vue
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/layout/components/Sidebar/Link.vue b/src/layout/components/Sidebar/Link.vue
new file mode 100644
index 0000000..2a796b8
--- /dev/null
+++ b/src/layout/components/Sidebar/Link.vue
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
diff --git a/src/layout/components/Sidebar/SidebarItem.vue b/src/layout/components/Sidebar/SidebarItem.vue
new file mode 100644
index 0000000..820970b
--- /dev/null
+++ b/src/layout/components/Sidebar/SidebarItem.vue
@@ -0,0 +1,166 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/layout/components/Sidebar/index.vue b/src/layout/components/Sidebar/index.vue
new file mode 100644
index 0000000..2400813
--- /dev/null
+++ b/src/layout/components/Sidebar/index.vue
@@ -0,0 +1,173 @@
+
+
+
+
+
+
+
+
diff --git a/src/router/index.js b/src/router/index.js
index 170105e..3b32773 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -2,186 +2,58 @@ import { createRouter, createWebHashHistory } from 'vue-router'
import { getToken, setToken } from '@/utils/auth'
import Login from '@/views/Login.vue'
import MainLayout from '@/layout/MainLayout.vue'
-import Dashboard from '@/views/Dashboard.vue'
-import BudgetList from '@/views/BudgetList.vue'
-import ExecutionList from '@/views/ExecutionList.vue'
-import Report from '@/views/Report.vue'
-// 事前流程
-import StartProcess from '@/views/pre-approval/StartProcess.vue'
-import ProcessQuery from '@/views/pre-approval/ProcessQuery.vue'
-
-// 支付流程
-import DirectPayment from '@/views/payment/DirectPayment.vue'
-import IndirectPayment from '@/views/payment/IndirectPayment.vue'
-import PaymentProcessQuery from '@/views/payment/ProcessQuery.vue'
-import PaymentQuery from '@/views/payment/PaymentQuery.vue'
-import PaymentDetailPrint from '@/views/payment/PaymentDetailPrint.vue'
-import DraftQuery from '@/views/payment/DraftQuery.vue'
-import CreatePayment from '@/views/payment/CreatePayment.vue'
-import ContractManagement from '@/views/payment/ContractManagement.vue'
-import ContractSettings from '@/views/payment/ContractSettings.vue'
-
-// 资金管理
-import Budget from '@/views/funds/Budget.vue'
-import BudgetManagement from '@/views/funds/BudgetManagement.vue'
-
-// 系统设置
-import CanvasSettings from '@/views/settings/CanvasSettings.vue'
-import PlannedExpenditureCategory from '@/views/settings/PlannedExpenditureCategory.vue'
-import PaymentCategory from '@/views/settings/PaymentCategory.vue'
-import TemplateElementSettings from '@/views/settings/TemplateElementSettings.vue'
-import PaymentTemplateElementSettings from '@/views/settings/PaymentTemplateElementSettings.vue'
-import PreApprovalTemplateSettings from '@/views/settings/PreApprovalTemplateSettings.vue'
-import PreApprovalProcessConfig from '@/views/settings/PreApprovalProcessConfig.vue'
-
-const routes = [
+/**
+ * 常量路由(无需权限,所有角色可访问)
+ */
+export const constantRoutes = [
{
path: '/login',
name: 'Login',
- component: Login
+ component: Login,
+ hidden: true
+ },
+ {
+ path: '/404',
+ name: '404',
+ component: () => import('@/views/404.vue'),
+ hidden: true
},
{
path: '/',
+ name: 'Layout',
component: MainLayout,
redirect: '/dashboard',
children: [
{
path: 'dashboard',
name: 'Dashboard',
- component: Dashboard
- },
- // 事前流程
- {
- path: 'pre-approval/start-process',
- name: 'StartProcess',
- component: StartProcess
- },
- {
- path: 'pre-approval/process-query',
- name: 'ProcessQuery',
- component: ProcessQuery
- },
- // 支付流程
- {
- path: 'payment/direct-payment',
- name: 'DirectPayment',
- component: DirectPayment
- },
- {
- path: 'payment/indirect-payment',
- name: 'IndirectPayment',
- component: IndirectPayment
- },
- {
- path: 'payment/process-query',
- name: 'PaymentProcessQuery',
- component: PaymentProcessQuery
- },
- {
- path: 'payment/payment-query',
- name: 'PaymentQuery',
- component: PaymentQuery
- },
- {
- path: 'payment/payment-detail-print/:id',
- name: 'PaymentDetailPrint',
- component: PaymentDetailPrint
- },
- {
- path: 'payment/draft-query',
- name: 'DraftQuery',
- component: DraftQuery
- },
- {
- path: 'payment/create-payment',
- name: 'CreatePayment',
- component: CreatePayment
- },
- {
- path: 'payment/contract-management',
- name: 'ContractManagement',
- component: ContractManagement
- },
- {
- path: 'settings/contract-settings',
- name: 'ContractSettings',
- component: ContractSettings
- },
- // 资金管理
- {
- path: 'funds/budget',
- name: 'Budget',
- component: Budget
- },
- {
- path: 'funds/budget-management',
- name: 'BudgetManagement',
- component: BudgetManagement
- },
- // 系统设置
- {
- path: 'settings/planned-expenditure-template-settings',
- name: 'CanvasSettings',
- component: CanvasSettings
- },
- {
- path: 'settings/planned-expenditure-category',
- name: 'PlannedExpenditureCategory',
- component: PlannedExpenditureCategory
- },
- {
- path: 'settings/payment-category',
- name: 'PaymentCategory',
- component: PaymentCategory
- },
- {
- path: 'settings/template-element-settings',
- name: 'TemplateElementSettings',
- component: TemplateElementSettings
- },
- {
- path: 'settings/payment-template-element-settings',
- name: 'PaymentTemplateElementSettings',
- component: PaymentTemplateElementSettings
- },
- {
- path: 'settings/pre-approval-template-settings',
- name: 'PreApprovalTemplateSettings',
- component: PreApprovalTemplateSettings
- },
- {
- path: 'settings/pre-approval-process-config',
- name: 'PreApprovalProcessConfig',
- component: PreApprovalProcessConfig
- },
- // 保留原有路由
- {
- path: 'budget-list',
- name: 'BudgetList',
- component: BudgetList
- },
- {
- path: 'execution-list',
- name: 'ExecutionList',
- component: ExecutionList
- },
- {
- path: 'report',
- name: 'Report',
- component: Report
+ component: () => import('@/views/Dashboard.vue'),
+ meta: {
+ title: '首页',
+ icon: 'el-icon-odometer'
+ }
}
]
}
]
-const router = createRouter({
- history: createWebHashHistory(),
- routes
-})
+/**
+ * 异步路由(需权限,动态加载)
+ */
+export const asyncRoutes = []
+
+const createRouterInstance = () => {
+ return createRouter({
+ history: createWebHashHistory(),
+ routes: constantRoutes
+ })
+}
+
+const router = createRouterInstance()
// 路由守卫 - 处理跨模块认证和登录状态检查
-router.beforeEach((to, from, next) => {
+router.beforeEach(async (to, from, next) => {
try {
// 1. 处理URL参数中的auth_token(跨模块跳转时携带)
if (to.query.auth_token) {
@@ -202,11 +74,86 @@ router.beforeEach((to, from, next) => {
: '/'
next({ path: redirectPath, replace: true })
} else {
- // 支持to参数进行路由跳转(跨模块跳转时使用)
- if (to.query.to && /^\/.*/.test(to.query.to)) {
- next({ path: to.query.to, replace: true })
+ // 动态加载路由
+ const { usePermissionStore } = await import('@/store/permission')
+ const permissionStore = usePermissionStore()
+
+ // 如果还没有生成路由,则生成
+ if (permissionStore.addRoutes.length === 0) {
+ try {
+ const accessedRoutes = await permissionStore.generateRoutes()
+ console.log('[Router] 获取到的路由:', accessedRoutes)
+
+ // 过滤出 404 路由和其他路由
+ const notFoundRoute = accessedRoutes.find(route => route.path === '/:pathMatch(.*)*')
+
+ /**
+ * 扁平化路由树,将一级菜单的 children 提取出来作为独立路由
+ * 因为 Vue Router 4 不能注册 path 为空的路由,但我们需要保留一级菜单结构用于菜单显示
+ */
+ const flattenRoutes = (routes) => {
+ const flattened = []
+ routes.forEach(route => {
+ // 跳过 404 路由
+ if (route.path === '/:pathMatch(.*)*') {
+ return
+ }
+
+ // 如果路由有 children 且 path 为空(文件夹路由),提取其 children
+ if (route.children && route.children.length > 0 && (!route.path || route.path === '')) {
+ // 递归处理 children,将它们扁平化
+ const childRoutes = flattenRoutes(route.children)
+ flattened.push(...childRoutes)
+ } else if (route.path && route.path !== '') {
+ // 如果路由有实际的 path,直接添加(但需要移除 children,因为已经扁平化了)
+ const { children, ...routeWithoutChildren } = route
+ flattened.push(routeWithoutChildren)
+ }
+ })
+ return flattened
+ }
+
+ const dynamicRoutes = flattenRoutes(accessedRoutes)
+
+ console.log('[Router] 动态路由数量:', dynamicRoutes.length)
+ console.log('[Router] 动态路由详情:', dynamicRoutes)
+
+ // 将动态路由作为主布局(name: 'Layout')的子路由添加
+ dynamicRoutes.forEach((route, index) => {
+ console.log(`[Router] 添加路由 ${index + 1}:`, route.path, route.name)
+ router.addRoute('Layout', route)
+ })
+
+ // 最后添加 404 路由(放在最后匹配)
+ if (notFoundRoute) {
+ router.addRoute(notFoundRoute)
+ }
+
+ // 打印所有路由用于调试
+ console.log('[Router] 当前所有路由:', router.getRoutes())
+
+ // 重新导航到目标路由,确保路由已添加
+ // 如果访问的是根路径,重定向到 dashboard
+ if (to.path === '/' || to.path === '') {
+ console.log('[Router] 重定向到 /dashboard')
+ next({ path: '/dashboard', replace: true })
+ } else {
+ console.log('[Router] 导航到:', to.path)
+ next({ ...to, replace: true })
+ }
+ } catch (error) {
+ console.error('[Router] 生成动态路由失败:', error)
+ console.error('[Router] 错误详情:', error)
+ // 如果生成路由失败,仍然允许访问
+ next()
+ }
} else {
- next()
+ // 支持to参数进行路由跳转(跨模块跳转时使用)
+ if (to.query.to && /^\/.*/.test(to.query.to)) {
+ next({ path: to.query.to, replace: true })
+ } else {
+ next()
+ }
}
}
} else {
@@ -234,5 +181,13 @@ router.beforeEach((to, from, next) => {
}
})
+/**
+ * 重置路由
+ */
+export function resetRouter() {
+ const newRouter = createRouterInstance()
+ router.matcher = newRouter.matcher
+}
+
export default router
diff --git a/src/store/permission.js b/src/store/permission.js
new file mode 100644
index 0000000..658e6d7
--- /dev/null
+++ b/src/store/permission.js
@@ -0,0 +1,214 @@
+import { defineStore } from 'pinia'
+import { permissionAPI } from '@/utils/api'
+import MainLayout from '@/layout/MainLayout.vue'
+
+/**
+ * 使用 import.meta.glob 预加载所有视图组件
+ * Vite 的限制:动态 import() 中的变量路径只能表示单层深度
+ * 因此使用 import.meta.glob 预加载所有视图文件
+ *
+ * import.meta.glob 返回的对象,键的格式可能是 '@/views/...' 或 '/src/views/...'
+ */
+const views = import.meta.glob('@/views/**/*.vue', { eager: false })
+
+// 在首次加载时打印所有可用的路径(用于调试)
+if (process.env.NODE_ENV === 'development') {
+ console.log('[Permission Store] 预加载的视图组件路径:', Object.keys(views))
+}
+
+/**
+ * 动态加载视图组件
+ */
+const loadView = (view) => {
+ // view 应该是类似 /pre-approval/StartProcess 这样的路径
+ console.log('[Permission Store] 加载视图:', view)
+ // 确保路径以 / 开头
+ const viewPath = view.startsWith('/') ? view : `/${view}`
+
+ // 尝试多种路径格式来匹配
+ const possiblePaths = [
+ `@/views${viewPath}.vue`, // @/views/pre-approval/StartProcess.vue
+ `/src/views${viewPath}.vue`, // /src/views/pre-approval/StartProcess.vue
+ `./src/views${viewPath}.vue`, // ./src/views/pre-approval/StartProcess.vue
+ `../src/views${viewPath}.vue`, // ../src/views/pre-approval/StartProcess.vue
+ ]
+
+ // 从预加载的 views 对象中查找对应的组件
+ for (const fullPath of possiblePaths) {
+ if (views[fullPath]) {
+ console.log('[Permission Store] 找到视图组件:', fullPath)
+ return views[fullPath]
+ }
+ }
+
+ // 如果所有格式都找不到,尝试模糊匹配(根据文件名)
+ const fileName = viewPath.split('/').pop()
+ const matchedKey = Object.keys(views).find(key => key.includes(`/${fileName}.vue`))
+ if (matchedKey) {
+ console.log('[Permission Store] 通过模糊匹配找到视图组件:', matchedKey)
+ return views[matchedKey]
+ }
+
+ console.warn('[Permission Store] 找不到视图组件:', viewPath, '尝试的路径:', possiblePaths)
+ console.warn('[Permission Store] 可用的路径:', Object.keys(views))
+ return null
+}
+
+/**
+ * 处理路由组件
+ */
+const componentHandle = (url, route) => {
+ // 如果 path 以 # 开头且 pid === 0,使用 MainLayout
+ if (/^#+/.test(route.path) && route.pid === 0) {
+ return MainLayout
+ } else if (/^#+/.test(route.path) && route.pid !== 0) {
+ // 嵌套布局(如果需要的话,可以创建 NestedLayout)
+ return MainLayout
+ } else {
+ // 处理 url 路径
+ let viewPath = url
+ if (!viewPath) {
+ console.warn('[Permission Store] 路由缺少 url 字段:', route)
+ return null
+ }
+
+ // 如果 url 是 #,说明这是一个文件夹,不需要组件
+ if (viewPath === '#') {
+ return null
+ }
+
+ // 如果 url 已经包含 @/views,去掉这个前缀(因为 loadView 会自动添加)
+ if (viewPath.startsWith('@/views')) {
+ viewPath = viewPath.replace('@/views', '')
+ }
+
+ // 如果 url 不是以 / 开头,添加 /
+ if (!viewPath.startsWith('/')) {
+ viewPath = '/' + viewPath
+ }
+
+ console.log('[Permission Store] 处理后的 viewPath:', viewPath, '原始 url:', url)
+ return loadView(viewPath)
+ }
+}
+
+/**
+ * 过滤和转换异步路由
+ */
+export function filterAsyncRoutes(routes) {
+ const res = []
+
+ routes.forEach(route => {
+ // 过滤不可见的路由
+ if (!route.visible) return
+
+ // 解析路径参数
+ const params = {}
+ if (route.path?.includes('?')) {
+ const flag = route.path.split('?')
+ route.path = flag[0]
+ if (flag[1]) {
+ const list = flag[1].split('&')
+ list.forEach(item => {
+ const kv = item.split('=')
+ if (kv.length === 2) {
+ params[kv[0]] = kv[1]
+ }
+ })
+ }
+ }
+
+ // 处理路径:如果 path 是 #,则设为空字符串(作为主布局的子路由)
+ // 保持路径原样,不去掉前导 /(与参考项目一致)
+ let routePath = route.path === '#' ? '' : route.path
+
+ // 如果 path 是 # 且 folder 是 true,这是一个文件夹菜单,不需要 component
+ const isFolder = route.path === '#' && route.folder
+
+ const tmp = {
+ key: `key-${route.id}`,
+ path: routePath, // 保持为空字符串,不要使用 folder-${route.id}
+ component: isFolder ? null : componentHandle(route.url, route), // 文件夹不需要 component
+ name: route.name,
+ hidden: !route.visible,
+ meta: {
+ title: route.title,
+ icon: route.icon,
+ guard: route.guard_name,
+ folder: route.folder,
+ params
+ }
+ }
+
+ // 递归处理子路由
+ if (route.children && route.children instanceof Array && route.children.length > 0) {
+ tmp.children = filterAsyncRoutes(route.children)
+ }
+
+ res.push(tmp)
+ })
+
+ return res
+}
+
+export const usePermissionStore = defineStore('permission', {
+ state: () => ({
+ routes: [],
+ addRoutes: []
+ }),
+
+ getters: {
+ permissionRoutes: (state) => state.routes,
+ addRoutesList: (state) => state.addRoutes
+ },
+
+ actions: {
+ /**
+ * 生成动态路由
+ */
+ async generateRoutes() {
+ try {
+ // 调用 API 获取权限路由数据
+ const response = await permissionAPI.getPermissions()
+ console.log('[Permission Store] API 返回数据:', response)
+
+ // 处理路由数据
+ let accessedRoutes = []
+ if (response.code === 0 && response.data) {
+ accessedRoutes = filterAsyncRoutes(response.data)
+ console.log('[Permission Store] 处理后的路由:', accessedRoutes)
+ } else {
+ console.warn('[Permission Store] API 返回数据格式不正确:', response)
+ }
+
+ // 添加 404 路由(放在最后匹配)
+ accessedRoutes.push({
+ path: '/:pathMatch(.*)*',
+ redirect: '/404',
+ hidden: true
+ })
+
+ // 保存路由
+ this.addRoutes = accessedRoutes
+ // 保存完整的路由结构(包括一级菜单),用于菜单显示
+ // 注意:这里保存的是 filterAsyncRoutes 返回的完整路由树,包括一级菜单(path 为空,有 children)
+ this.routes = accessedRoutes.filter(route => route.path !== '/:pathMatch(.*)*')
+
+ console.log('[Permission Store] 最终路由列表(用于菜单显示):', this.routes)
+ return Promise.resolve(accessedRoutes)
+ } catch (err) {
+ console.error('[Permission Store] 生成动态路由失败:', err)
+ return Promise.reject(err)
+ }
+ },
+
+ /**
+ * 重置路由
+ */
+ resetRoutes() {
+ this.routes = []
+ this.addRoutes = []
+ }
+ }
+})
+
diff --git a/src/utils/api.js b/src/utils/api.js
index f760d31..34e7a6e 100644
--- a/src/utils/api.js
+++ b/src/utils/api.js
@@ -150,8 +150,6 @@ export const reportAPI = {
}
}
-// 分支条件相关 API 已移除
-
/**
* 计划支出分类相关 API
*/
@@ -181,11 +179,6 @@ export const plannedExpenditureCategoryAPI = {
return request.get('/budget/planned-expenditure-categories-tree')
},
- // 获取分类详情
- getDetail: (id) => {
- return request.get(`/budget/planned-expenditure-categories/${id}`)
- },
-
// 创建分类
create: (data) => {
return request.post('/budget/planned-expenditure-categories', data)
@@ -204,9 +197,7 @@ export const plannedExpenditureCategoryAPI = {
// 批量删除分类
batchDelete: (ids) => {
return request.post('/budget/planned-expenditure-categories/batch-destroy', { ids })
- },
-
- // 获取分类的分支条件详情 - 已移除,使用分类层级关系替代
+ }
}
/**
@@ -677,7 +668,6 @@ export const expenditureQueryAPI = {
}
}
-
/**
* 认证相关 API
* 注意:baseURL 已配置为 /api,所以这里不需要再加 /api 前缀
@@ -702,31 +692,25 @@ export const authAPI = {
}
}
-// 导出便捷方法(保持向后兼容)
-export const login = authAPI.login
-export const getInfo = authAPI.getInfo
-export const logout = authAPI.logout
-
/**
- * 使用示例:
- *
- * import { budgetAPI } from '@/utils/api'
- *
- * // 在组件中使用
- * const fetchBudgetList = async () => {
- * try {
- * const data = await budgetAPI.getBudgetList({ page: 1, pageSize: 10 })
- * console.log(data)
- * } catch (error) {
- * console.error('获取预算列表失败:', error)
- * }
- * }
- *
- * 注意:
- * - 所有 API 请求都会自动使用 config.api.baseURL 作为基础地址
- * - 开发环境下如果启用了调试模式,会在控制台打印请求和响应信息
- * - 请求超时时间由 config.api.timeout 控制
+ * 权限相关 API
*/
+export const permissionAPI = {
+ // 获取权限路由数据
+ getPermissions: () => {
+ // 获取模块名称,优先级:window.MODULE_NAME > 从路径提取 > 环境变量
+ const moduleName = window.MODULE_NAME ||
+ window.location.pathname.replaceAll(/\//g, '') ||
+ import.meta.env.VITE_APP_MODULE_NAME ||
+ 'budget'
+
+ return request.get(`/auth/module-permissions/${moduleName}`, {
+ module: moduleName
+ }, {
+ isLoading: false
+ })
+ }
+}
/**
* OA会议纪要相关 API
@@ -743,3 +727,7 @@ export const oaMeetingMinutesAPI = {
}
}
+// 导出便捷方法(保持向后兼容)
+export const login = authAPI.login
+export const getInfo = authAPI.getInfo
+export const logout = authAPI.logout
diff --git a/src/utils/validate.js b/src/utils/validate.js
new file mode 100644
index 0000000..a411e24
--- /dev/null
+++ b/src/utils/validate.js
@@ -0,0 +1,23 @@
+/**
+ * 验证工具函数
+ */
+
+/**
+ * 判断是否为外部链接
+ * @param {string} path - 路径
+ * @returns {Boolean}
+ */
+export function isExternal(path) {
+ return /^(https?:|mailto:|tel:)/.test(path)
+}
+
+/**
+ * 验证用户名(可选,保留向后兼容)
+ * @param {string} str - 用户名
+ * @returns {Boolean}
+ */
+export function validUsername(str) {
+ const valid_map = ['admin', 'editor']
+ return valid_map.indexOf(str.trim()) >= 0
+}
+
diff --git a/src/views/404.vue b/src/views/404.vue
new file mode 100644
index 0000000..b56c53a
--- /dev/null
+++ b/src/views/404.vue
@@ -0,0 +1,242 @@
+
+
+
+
+
+
OOPS!
+
{{ message }}
+
请检查您输入的网址是否正确,或者单击下面的按钮返回主页。
+
返回首页
+
+
+
+
+
+
+
+
+
\ No newline at end of file