调试frontend

This commit is contained in:
2026-07-21 14:04:49 +08:00
parent dd1e55aea3
commit b45152599e
18 changed files with 1840 additions and 1382 deletions
+3 -3
View File
@@ -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
}
+28 -39
View File
@@ -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
View File
@@ -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,
},
})
}
}
+7 -3
View File
@@ -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)
+80 -16
View File
@@ -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 {
+1 -1
View File
@@ -188,7 +188,7 @@ const formatDate = (date: string) => {
}
const goToDetail = (id: number) => {
router.push(`/newscenter/companyNews/detail/${id}`)
router.push(`/newsCenter/companyNews/detail/${id}`)
}
// 加载更多数据
+5 -1
View File
@@ -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: []
}
}
})
})