diff --git a/backend/src/api/frontMenu.js b/backend/src/api/frontMenu.js index f3f4dac..4124551 100644 --- a/backend/src/api/frontMenu.js +++ b/backend/src/api/frontMenu.js @@ -16,14 +16,11 @@ export function getFrontMenus() { * @param {Object} frontMenuData 前端导航数据 * @returns {Promise} */ -export function createFrontMenu(formData, options = {}) { +export function createFrontMenu(formData) { return request({ - url: "/backend/createfrontmenu", + url: "/backend/frontmenus", method: "post", data: formData, - headers: { - "Content-Type": "multipart/form-data" - } }); } @@ -35,7 +32,7 @@ export function createFrontMenu(formData, options = {}) { */ export function editFrontMenu(id, frontMenuData) { return request({ - url: `/backend/editfrontmenu/${id}`, + url: `/backend/frontmenus/${id}`, method: "post", data: frontMenuData, }); @@ -48,7 +45,7 @@ export function editFrontMenu(id, frontMenuData) { */ export function deleteFrontMenu(id) { return request({ - url: `/backend/deletefrontmenu/${id}`, + url: `/backend/frontmenus/${id}`, method: "delete", }); } diff --git a/backend/src/components/CommonAside.vue b/backend/src/components/CommonAside.vue index 5b55ba0..4335495 100644 --- a/backend/src/components/CommonAside.vue +++ b/backend/src/components/CommonAside.vue @@ -6,7 +6,7 @@ -
+
{{ errorMsg }}
重新加载 @@ -289,13 +289,40 @@ const processMenus = (menus) => { })); }; -const list = computed(() => { - const menuData = menuStore.menus; - if (!menuData || menuData.length === 0) { - return []; - } +const fixedCmsMenu = { + id: -200, + path: "/apps/cms", + title: "文章中心", + icon: "Document", + order: -200, + children: [ + { + id: -201, + path: "/apps/cms/articles", + title: "文章管理", + icon: "Document", + order: 1, + children: [] + }, + { + id: -202, + path: "/apps/cms/articles/category", + title: "文章分类", + icon: "Folder", + order: 2, + children: [] + } + ] +}; - const allMenus = processMenus(menuData); +const mergeFixedCmsMenu = (menus) => { + const hasCmsMenu = menus.some((menu) => menu.path === fixedCmsMenu.path); + return hasCmsMenu ? menus : [fixedCmsMenu, ...menus]; +}; + +const list = computed(() => { + const menuData = menuStore.menus || []; + const allMenus = processMenus(mergeFixedCmsMenu(menuData)); const sortMenusRecursively = (menus) => { menus.forEach((menu) => { diff --git a/backend/src/router/index.js b/backend/src/router/index.js index 8798c45..2eec62b 100644 --- a/backend/src/router/index.js +++ b/backend/src/router/index.js @@ -3,6 +3,19 @@ import { convertMenusToRoutes } from "./dynamicRoutes"; // 静态子路由:需要在 Main 框架内显示的页面 const staticMainChildren = [ + // CMS 文章中心是系统内置功能,不依赖数据库菜单配置。 + { + path: "/apps/cms/articles", + name: "CmsArticles", + component: () => import("@/views/apps/cms/articles/index.vue"), + meta: { requiresAuth: true, title: "文章管理", modulePath: "/apps/cms" } + }, + { + path: "/apps/cms/articles/category", + name: "CmsArticleCategories", + component: () => import("@/views/apps/cms/articles/category.vue"), + meta: { requiresAuth: true, title: "文章分类", modulePath: "/apps/cms" } + }, { path: "/user/userProfile", name: "userProfile", @@ -90,6 +103,8 @@ export async function loadAndAddDynamicRoutes() { routesLoadingPromise = null; return Promise.resolve(); } else { + // 即使权限菜单为空,也要注册内置文章中心路由。 + addDynamicRoutes([]); dynamicRoutesAdded = true; routesLoadingPromise = null; return Promise.resolve(); diff --git a/backend/src/views/apps/cms/articles/category.vue b/backend/src/views/apps/cms/articles/category.vue index b654ab0..70ba02d 100644 --- a/backend/src/views/apps/cms/articles/category.vue +++ b/backend/src/views/apps/cms/articles/category.vue @@ -156,6 +156,11 @@ function toggleExpand(category) { } function handleDelete(category) { + if (Number(category.tid) === 0 || category.is_global === true) { + ElMessage.warning("全局分类不可删除"); + return; + } + ElMessageBox.confirm(`确定要删除分类\"${category.label}\"吗?`, "提示", { confirmButtonText: "确定", cancelButtonText: "取消", diff --git a/backend/src/views/apps/cms/articles/components/CategoryNode.vue b/backend/src/views/apps/cms/articles/components/CategoryNode.vue index b08db73..0f8b4cb 100644 --- a/backend/src/views/apps/cms/articles/components/CategoryNode.vue +++ b/backend/src/views/apps/cms/articles/components/CategoryNode.vue @@ -53,9 +53,18 @@ - + + + 全局 +
diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 524e80c..6591aef 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -2,18 +2,18 @@ import service from '@/utils/request' // 获取前端导航 export const getHeadMenu = async () => { - const response = await service.get('index/headmenu') + const response = await service.get('/api/index/headmenu') return response.data } // 根据路径获取单页内容 export const getOnePageByPath = async (path: string) => { - const response = await service.get(`index/onepage/${encodeURIComponent(path)}`) + const response = await service.get(`/api/index/onepage/${encodeURIComponent(path)}`) return response.data } // 获取前端底部数据 export const getFooterData = async () => { - const response = await service.get('index/footerdata') + const response = await service.get('/api/index/footerdata') return response.data } \ No newline at end of file diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 4dceab5..462b5fd 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -1,52 +1,41 @@ -import { createRouter, createWebHistory } from 'vue-router'; -import { constantRoute, registerDynamicRoutes } from './routes'; +import { createRouter, createWebHistory } from 'vue-router' +import { getHeadMenu } from '@/api/index' +import { constantRoute, registerDynamicRoutes } from './routes' -// 先创建路由,但不立即匹配 const router = createRouter({ history: createWebHistory(), routes: constantRoute, - // 延迟路由匹配,等待动态路由注册完成 strict: false, -}); +}) -// 在应用启动时加载菜单并注册路由 -let routesLoaded = false; -let routesLoadingPromise: Promise | null = null; +let routesLoaded = false +let routesLoadingPromise: Promise | null = null +// 固定路由先可用,再尝试加载后台菜单补充自定义页面。 export async function loadAndRegisterRoutes() { - // 如果已经加载过,直接返回 - if (routesLoaded) { - return; - } + if (routesLoaded) return + if (routesLoadingPromise) return routesLoadingPromise - // 如果正在加载,等待加载完成 - if (routesLoadingPromise) { - return routesLoadingPromise; - } + routesLoadingPromise = getHeadMenu() + .then((response) => { + if (response?.code === 200 && Array.isArray(response.data)) { + registerDynamicRoutes(router, response.data) + } + }) + .catch((error) => { + // 后台菜单不可用时不影响固定文章路由和官网页面。 + console.error('加载前端菜单失败:', error) + }) + .finally(() => { + routesLoaded = true + }) - return routesLoadingPromise; + return routesLoadingPromise } -// 路由守卫:确保路由已加载 -router.beforeEach(async (to, _from, next) => { - // 如果路由还未加载,先加载路由 - if (!routesLoaded) { - - // 等待路由加载完成 - await routesLoadingPromise; - - // 路由加载完成后,如果当前路径不匹配,尝试重新匹配 - const matched = router.resolve(to.path).matched; - if (matched.length === 0 && to.path !== '/404') { - // 路由已加载但当前路径不匹配,可能是通配符路由 - // 继续导航,让通配符路由处理 - next(); - return; - } - } +router.beforeEach(async (_to, _from, next) => { + await loadAndRegisterRoutes() + next() +}) - // 路由已加载,继续导航 - next(); -}); - -export default router; \ No newline at end of file +export default router diff --git a/frontend/src/router/routes.ts b/frontend/src/router/routes.ts index 6efa39a..7199ded 100644 --- a/frontend/src/router/routes.ts +++ b/frontend/src/router/routes.ts @@ -1,124 +1,119 @@ import { createComponentLoader } from '@/utils/pathResolver' -// 对外暴漏配置路由,常量路由 +// 对外暴露配置路由。核心业务路由必须固定存在,不依赖后台菜单配置。 export const constantRoute = [ - { - path: '/', - component: () => import('@/views/home/index.vue'), - name: '首页', - meta: { - title: '首页', - hidden: false, - }, - }, - { - path: '/404', - component: () => import('@/views/404/index.vue'), - name: '404', - meta: { - title: '404', - hidden: true, - icon: 'DocumentDelete', - }, - }, - ] + { + path: '/', + component: () => import('@/views/home/index.vue'), + name: '首页', + meta: { title: '首页', hidden: false }, + }, + { + path: '/solution', + component: () => import('@/views/solution/index.vue'), + name: '解决方案', + meta: { title: '解决方案', hidden: false }, + }, + { + path: '/solution/solution', + component: () => import('@/views/solutions/solution/index.vue'), + name: '行业解决方案', + meta: { title: '行业解决方案', hidden: true }, + }, + { + path: '/solution/successCases', + component: () => import('@/views/solutions/successCases/index.vue'), + name: '成功案例', + meta: { title: '成功案例', hidden: true }, + }, + { + path: '/newsCenter', + component: () => import('@/views/newsCenter/news/index.vue'), + name: '新闻中心', + meta: { title: '新闻中心', hidden: false }, + }, + { + path: '/newsCenter/news', + component: () => import('@/views/newsCenter/news/index.vue'), + name: '站点新闻', + meta: { title: '站点新闻', hidden: true }, + }, + { + path: '/newsCenter/announcement', + component: () => import('@/views/newsCenter/announcement/index.vue'), + name: '站点公告', + meta: { title: '站点公告', hidden: true }, + }, + { + path: '/newsCenter/technologyCenter', + component: () => import('@/views/newsCenter/technologyCenter/index.vue'), + name: '技术中心', + meta: { title: '技术中心', hidden: true }, + }, + // 文章详情路由固定注册,文章发布后无需先在前台菜单中配置路由。 + { + path: '/newsCenter/companyNews/detail/:id', + component: () => import('@/views/components/articleDetail.vue'), + name: '公司新闻详情', + meta: { title: '公司新闻详情', hidden: true }, + }, + { + path: '/newsCenter/kingdeeNews/detail/:id', + component: () => import('@/views/components/articleDetail.vue'), + name: '金蝶新闻详情', + meta: { title: '金蝶新闻详情', hidden: true }, + }, + { + path: '/404', + component: () => import('@/views/404/index.vue'), + name: '404', + meta: { title: '404', hidden: true, icon: 'DocumentDelete' }, + }, + { + path: '/:pathMatch(.*)*', + component: () => import('@/views/onepage/index.vue'), + name: 'OnePage', + meta: { title: '单页', hidden: true }, + }, +] -// 动态路由注册函数 +// 动态路由注册函数:后台菜单只负责补充自定义页面,不覆盖固定业务路由。 export function registerDynamicRoutes(router: any, menus: any[]) { if (!menus || menus.length === 0) return - // 统一处理菜单路径大小写 - const normalizeMenuPaths = (menus: any[]) => { - return menus.map(menu => { - const normalizedMenu = { ...menu } - // 将路径中的 /newscenter/ 替换为 /newsCenter/ - if (normalizedMenu.path) { - normalizedMenu.path = normalizedMenu.path.replace(/\/newscenter\//g, '/newsCenter/') - } - // 递归处理子菜单 - if (normalizedMenu.children && normalizedMenu.children.length > 0) { - normalizedMenu.children = normalizeMenuPaths(normalizedMenu.children) - } - return normalizedMenu - }) - } + const normalizeMenuPaths = (items: any[]): any[] => items.map((menu) => ({ + ...menu, + path: menu.path?.replace(/\/newscenter\//g, '/newsCenter/'), + children: menu.children?.length ? normalizeMenuPaths(menu.children) : menu.children, + })) - // 先规范化菜单路径 - const normalizedMenus = normalizeMenuPaths(menus) - - // 收集所有需要注册的路由 const routesToAdd: any[] = [] - - // 递归处理菜单,收集路由 const processMenu = (menu: any) => { - // type 2: 页面 - 根据 component_path 加载组件 if (menu.type === 2 && menu.path && menu.component_path) { const path = menu.path - // 检查路由是否已存在 - const existingRoute = router.getRoutes().find((r: any) => r.path === path) - if (!existingRoute) { - try { - // 使用 pathResolver 来解析 component_path - // component_path 可能的格式: - // 1. /views/newsCenter/companyNews/index.vue - // 2. views/newsCenter/companyNews/index.vue - // 3. newsCenter/companyNews/index.vue - const componentLoader = createComponentLoader(menu.component_path) - - if (!componentLoader) { - console.error(`Failed to create component loader for: ${menu.component_path}`) - return - } - - routesToAdd.push({ - path: path, - name: `menu_${menu.id}`, - component: componentLoader, - meta: { - title: menu.title, - menuId: menu.id, - menuType: menu.type, - }, - }) - } catch (error) { - console.error(`Failed to prepare route for path ${path}, component_path: ${menu.component_path}`, error) - } + if (!router.getRoutes().some((route: any) => route.path === path)) { + routesToAdd.push({ + path, + name: `menu_${menu.id}`, + component: createComponentLoader(menu.component_path), + meta: { title: menu.title, menuId: menu.id, menuType: menu.type }, + }) } } - // type 4: 单页 - 使用通配符路由处理,不需要单独注册 - - // 递归处理子菜单 - if (menu.children && menu.children.length > 0) { - menu.children.forEach((child: any) => processMenu(child)) - } + menu.children?.forEach(processMenu) } - // 处理所有菜单 - normalizedMenus.forEach((menu) => processMenu(menu)) + normalizeMenuPaths(menus).forEach(processMenu) - // 先移除通配符路由(如果存在),以便重新添加 - const catchAllRoute = router.getRoutes().find((r: any) => r.path === '/:pathMatch(.*)*') - if (catchAllRoute) { - router.removeRoute('OnePage') - } + // 通配符路由必须最后注册,否则会抢先匹配后台新增的自定义页面。 + const catchAll = router.getRoutes().find((route: any) => route.name === 'OnePage') + if (catchAll) router.removeRoute('OnePage') - // 注册 type 2 的路由(必须在通配符路由之前) - routesToAdd.forEach((route) => { - router.addRoute(route) + routesToAdd.forEach((route) => router.addRoute(route)) + router.addRoute({ + path: '/:pathMatch(.*)*', + component: () => import('@/views/onepage/index.vue'), + name: 'OnePage', + meta: { title: '单页', hidden: true }, }) - - // 最后添加通配符路由(用于单页和其他未匹配的路径) - const catchAllExists = router.getRoutes().find((r: any) => r.path === '/:pathMatch(.*)*') - if (!catchAllExists) { - router.addRoute({ - path: '/:pathMatch(.*)*', - component: () => import('@/views/onepage/index.vue'), - name: 'OnePage', - meta: { - title: '单页', - hidden: true, - }, - }) - } } - \ No newline at end of file diff --git a/frontend/src/utils/request.ts b/frontend/src/utils/request.ts index c362bf5..b612182 100644 --- a/frontend/src/utils/request.ts +++ b/frontend/src/utils/request.ts @@ -2,13 +2,17 @@ import axios from 'axios' // 创建axios实例 const service = axios.create({ - baseURL: import.meta.env.VITE_API_BASE_URL, + baseURL: '/', timeout: 10000, }) // 请求拦截器 service.interceptors.request.use( (config) => { + const devTenantId = sessionStorage.getItem('dev_tenant_id') + if (devTenantId) { + config.headers['X-Tenant-ID'] = encodeURIComponent(devTenantId) + } return config }, (error) => { @@ -16,10 +20,10 @@ service.interceptors.request.use( } ) -// 响应拦截器 - 返回 response.data +// 响应拦截器 - 保留完整 axios 响应,由 api 层统一取 response.data(即 {code, data, msg}) service.interceptors.response.use( (response) => { - return response.data + return response }, (error) => { console.error('API请求错误:', error) diff --git a/frontend/src/views/components/header.vue b/frontend/src/views/components/header.vue index 3c1afd3..35e591a 100644 --- a/frontend/src/views/components/header.vue +++ b/frontend/src/views/components/header.vue @@ -301,12 +301,36 @@
+ +
+ +
中文
+ + +