调试frontend
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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<void> | null = null;
|
||||
let routesLoaded = false
|
||||
let routesLoadingPromise: Promise<void> | 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;
|
||||
export default router
|
||||
|
||||
+99
-104
@@ -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,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -301,12 +301,36 @@
|
||||
<div class="search-icon" @click="showSearch = true">
|
||||
<i class="fa-solid fa-magnifying-glass"></i>
|
||||
</div>
|
||||
<!-- 开发环境租户切换按钮 -->
|
||||
<div v-if="isDev" class="tenant-switcher" @click="showTenantModal = true">
|
||||
<i class="fa-solid fa-gear"></i>
|
||||
</div>
|
||||
<div class="language-switcher" @click="showLanguage = true">
|
||||
<span>中文</span>
|
||||
<i class="fa-solid fa-caret-down"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 租户切换弹窗 -->
|
||||
<div
|
||||
class="modal"
|
||||
:class="{ active: showTenantModal }"
|
||||
@click.self="showTenantModal = false"
|
||||
>
|
||||
<div class="modal-content">
|
||||
<h3>切换测试租户</h3>
|
||||
<div class="search-box">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="请输入租户ID..."
|
||||
v-model="devTenantId"
|
||||
/>
|
||||
<button @click="saveTenant">保存</button>
|
||||
</div>
|
||||
<button class="close-btn" @click="showTenantModal = false">×</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<div
|
||||
class="modal"
|
||||
@@ -390,33 +414,62 @@ interface MenuItem {
|
||||
// 菜单数据
|
||||
const menuItems = ref<MenuItem[]>([])
|
||||
|
||||
// 统一处理菜单路径大小写
|
||||
// 统一处理菜单路径大小写,并递归处理子菜单。
|
||||
const normalizeMenuPaths = (menus: MenuItem[]): MenuItem[] => {
|
||||
return menus.map((menu: MenuItem) => {
|
||||
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
|
||||
})
|
||||
return menus.map((menu) => ({
|
||||
...menu,
|
||||
path: menu.path?.replace(/\/newscenter\//g, '/newsCenter/'),
|
||||
children: menu.children?.length ? normalizeMenuPaths(menu.children) : menu.children,
|
||||
}))
|
||||
}
|
||||
|
||||
// 后台菜单可配置,但文章中心是官网核心功能,必须始终存在。
|
||||
const fixedNewsMenu: MenuChild = {
|
||||
id: -101,
|
||||
title: '新闻中心',
|
||||
path: '/newsCenter/news',
|
||||
type: 2,
|
||||
}
|
||||
|
||||
const fixedArticleMenu: MenuItem = {
|
||||
id: -100,
|
||||
title: '文章中心',
|
||||
path: '/newsCenter',
|
||||
type: 1,
|
||||
children: [fixedNewsMenu],
|
||||
}
|
||||
|
||||
const mergeMenuItems = (menus: MenuItem[]): MenuItem[] => {
|
||||
const normalizedMenus = normalizeMenuPaths(menus)
|
||||
const articleMenu = normalizedMenus.find(
|
||||
(menu) => menu.path === fixedArticleMenu.path || menu.title === fixedArticleMenu.title,
|
||||
)
|
||||
if (!articleMenu) return [fixedArticleMenu, ...normalizedMenus]
|
||||
|
||||
const hasNewsCenter = articleMenu.children?.some(
|
||||
(child) => child.path === '/newsCenter/news' || child.title === '新闻中心',
|
||||
)
|
||||
if (!hasNewsCenter) {
|
||||
articleMenu.children = [
|
||||
...(articleMenu.children || []),
|
||||
fixedNewsMenu,
|
||||
]
|
||||
}
|
||||
return normalizedMenus
|
||||
}
|
||||
|
||||
// 加载菜单数据
|
||||
const loadMenu = async () => {
|
||||
try {
|
||||
const response = await getHeadMenu()
|
||||
if (response.code === 200) {
|
||||
menuItems.value = normalizeMenuPaths(response.data)
|
||||
if (response.code === 200 && Array.isArray(response.data)) {
|
||||
menuItems.value = mergeMenuItems(response.data)
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载菜单失败:', error)
|
||||
menuItems.value = []
|
||||
}
|
||||
menuItems.value = [fixedArticleMenu]
|
||||
}
|
||||
|
||||
// 拼接接口地址(用于 logo 等静态资源)
|
||||
@@ -436,9 +489,18 @@ const activeMenu = ref<number | null>(null)
|
||||
const activeCategory = ref<number>(0)
|
||||
const showSearch = ref(false)
|
||||
const showLanguage = ref(false)
|
||||
const showTenantModal = ref(false)
|
||||
const devTenantId = ref(sessionStorage.getItem('dev_tenant_id') || '')
|
||||
const isDev = import.meta.env.DEV
|
||||
const searchQuery = ref('')
|
||||
const currentLanguage = ref('zh')
|
||||
const isScrolled = ref(false)
|
||||
|
||||
const saveTenant = () => {
|
||||
sessionStorage.setItem('dev_tenant_id', devTenantId.value)
|
||||
showTenantModal.value = false
|
||||
window.location.reload()
|
||||
}
|
||||
const footerData = ref<any | null>(null)
|
||||
let closeTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
@@ -834,6 +896,8 @@ onUnmounted(() => {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
color: #333; /* 显式设置字体颜色为深色 */
|
||||
background-color: #fff; /* 显式设置背景为白色 */
|
||||
transition: border-color 0.3s ease;
|
||||
|
||||
&:focus {
|
||||
|
||||
@@ -188,7 +188,7 @@ const formatDate = (date: string) => {
|
||||
}
|
||||
|
||||
const goToDetail = (id: number) => {
|
||||
router.push(`/newscenter/companyNews/detail/${id}`)
|
||||
router.push(`/newsCenter/companyNews/detail/${id}`)
|
||||
}
|
||||
|
||||
// 加载更多数据
|
||||
|
||||
@@ -61,6 +61,10 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
|
||||
target: env.VITE_API_BASE_URL, // 后端 API 服务器地址
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
},
|
||||
'/index': {
|
||||
target: env.VITE_API_BASE_URL,
|
||||
changeOrigin: true
|
||||
}
|
||||
},
|
||||
// 配置静态资源服务
|
||||
@@ -91,4 +95,4 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
|
||||
exclude: []
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user