diff --git a/Yi.Ai.Vue3/.build/plugins/fontawesome.ts b/Yi.Ai.Vue3/.build/plugins/fontawesome.ts new file mode 100644 index 00000000..22207eda --- /dev/null +++ b/Yi.Ai.Vue3/.build/plugins/fontawesome.ts @@ -0,0 +1,23 @@ +import type { Plugin } from 'vite'; +import { config, library } from '@fortawesome/fontawesome-svg-core'; +import { fas } from '@fortawesome/free-solid-svg-icons'; + +/** + * Vite 插件:配置 FontAwesome + * 预注册所有图标,避免运行时重复注册 + */ +export default function fontAwesomePlugin(): Plugin { + // 在模块加载时配置 FontAwesome + library.add(fas); + + return { + name: 'vite-plugin-fontawesome', + config() { + return { + define: { + // 确保 FontAwesome 在客户端正确初始化 + }, + }; + }, + }; +} diff --git a/Yi.Ai.Vue3/.build/plugins/index.ts b/Yi.Ai.Vue3/.build/plugins/index.ts index 122c0fe2..bed77970 100644 --- a/Yi.Ai.Vue3/.build/plugins/index.ts +++ b/Yi.Ai.Vue3/.build/plugins/index.ts @@ -10,15 +10,21 @@ import Components from 'unplugin-vue-components/vite'; import viteCompression from 'vite-plugin-compression'; import envTyped from 'vite-plugin-env-typed'; +import fontAwesomePlugin from './fontawesome'; import gitHashPlugin from './git-hash'; +import preloadPlugin from './preload'; import createSvgIcon from './svg-icon'; +import versionHtmlPlugin from './version-html'; const root = path.resolve(__dirname, '../../'); function plugins({ mode, command }: ConfigEnv): PluginOption[] { return [ + versionHtmlPlugin(), // 最先处理 HTML 版本号 gitHashPlugin(), + preloadPlugin(), UnoCSS(), + fontAwesomePlugin(), envTyped({ mode, envDir: root, @@ -35,7 +41,18 @@ function plugins({ mode, command }: ConfigEnv): PluginOption[] { dts: path.join(root, 'types', 'auto-imports.d.ts'), }), Components({ - resolvers: [ElementPlusResolver()], + resolvers: [ + ElementPlusResolver(), + // 自动导入 FontAwesomeIcon 组件 + (componentName) => { + if (componentName === 'FontAwesomeIcon') { + return { + name: 'FontAwesomeIcon', + from: '@/components/FontAwesomeIcon/index.vue', + }; + } + }, + ], dts: path.join(root, 'types', 'components.d.ts'), }), createSvgIcon(command === 'build'), diff --git a/Yi.Ai.Vue3/.build/plugins/preload.ts b/Yi.Ai.Vue3/.build/plugins/preload.ts new file mode 100644 index 00000000..4bccc738 --- /dev/null +++ b/Yi.Ai.Vue3/.build/plugins/preload.ts @@ -0,0 +1,47 @@ +import type { Plugin } from 'vite'; + +/** + * Vite 插件:资源预加载优化 + * 自动添加 Link 标签预加载关键资源 + */ +export default function preloadPlugin(): Plugin { + return { + name: 'vite-plugin-preload-optimization', + apply: 'build', + transformIndexHtml(html, context) { + // 只在生产环境添加预加载 + if (process.env.NODE_ENV === 'development') { + return html; + } + + const bundle = context.bundle || {}; + const preloadLinks: string[] = []; + + // 收集关键资源 + const criticalChunks = ['vue-vendor', 'element-plus', 'pinia']; + const criticalAssets: string[] = []; + + Object.entries(bundle).forEach(([fileName, chunk]) => { + if (chunk.type === 'chunk' && criticalChunks.some(name => fileName.includes(name))) { + criticalAssets.push(`/${fileName}`); + } + }); + + // 生成预加载标签 + criticalAssets.forEach(href => { + if (href.endsWith('.js')) { + preloadLinks.push(``); + } else if (href.endsWith('.css')) { + preloadLinks.push(``); + } + }); + + // 将预加载标签插入到 之前 + if (preloadLinks.length > 0) { + return html.replace('', `${preloadLinks.join('\n ')}\n`); + } + + return html; + }, + }; +} diff --git a/Yi.Ai.Vue3/.build/plugins/version-html.ts b/Yi.Ai.Vue3/.build/plugins/version-html.ts new file mode 100644 index 00000000..674dd304 --- /dev/null +++ b/Yi.Ai.Vue3/.build/plugins/version-html.ts @@ -0,0 +1,20 @@ +import type { Plugin } from 'vite'; +import { APP_VERSION, APP_NAME } from '../../src/config/version'; + +/** + * Vite 插件:在 HTML 中注入版本号 + * 替换 HTML 中的占位符为实际版本号 + */ +export default function versionHtmlPlugin(): Plugin { + return { + name: 'vite-plugin-version-html', + enforce: 'pre', + transformIndexHtml(html) { + // 替换 HTML 中的版本占位符 + return html + .replace(/%APP_NAME%/g, APP_NAME) + .replace(/%APP_VERSION%/g, APP_VERSION) + .replace(/%APP_FULL_NAME%/g, `${APP_NAME} ${APP_VERSION}`); + }, + }; +} diff --git a/Yi.Ai.Vue3/FONTAWESOME_MIGRATION.md b/Yi.Ai.Vue3/FONTAWESOME_MIGRATION.md new file mode 100644 index 00000000..b5ef1146 --- /dev/null +++ b/Yi.Ai.Vue3/FONTAWESOME_MIGRATION.md @@ -0,0 +1,133 @@ +# FontAwesome 图标迁移指南 + +## 迁移步骤 + +### 1. 在组件中使用 FontAwesomeIcon + +```vue + + + + + + + + +``` + +```vue + + + + + + + + + + + +``` + +### 2. 自动映射工具 + +使用 `getFontAwesomeIcon` 函数自动映射图标名: + +```typescript +import { getFontAwesomeIcon } from '@/utils/icon-mapping'; + +// 将 Element Plus 图标名转换为 FontAwesome 图标名 +const faIcon = getFontAwesomeIcon('Check'); // 返回 'check' +const faIcon2 = getFontAwesomeIcon('ArrowLeft'); // 返回 'arrow-left' +``` + +### 3. Props 说明 + +| Prop | 类型 | 可选值 | 说明 | +|------|------|--------|------| +| icon | string | 任意 FontAwesome 图标名 | 图标名称(不含 fa- 前缀) | +| size | string | xs, sm, lg, xl, 2x, 3x, 4x, 5x | 图标大小 | +| spin | boolean | true/false | 是否旋转 | +| pulse | boolean | true/false | 是否脉冲动画 | +| rotation | number | 0, 90, 180, 270 | 旋转角度 | + +### 4. 常用图标对照表 + +| Element Plus | FontAwesome | +|--------------|-------------| +| Check | check | +| Close | xmark | +| Delete | trash | +| Edit | pen-to-square | +| Plus | plus | +| Search | magnifying-glass | +| Refresh | rotate-right | +| Loading | spinner | +| Download | download | +| ArrowLeft | arrow-left | +| ArrowRight | arrow-right | +| User | user | +| Setting | gear | +| View | eye | +| Hide | eye-slash | +| Lock | lock | +| Share | share-nodes | +| Heart | heart | +| Star | star | +| Clock | clock | +| Calendar | calendar | +| ChatLineRound | comment | +| Bell | bell | +| Document | file | +| Picture | image | + +### 5. 批量迁移示例 + +```vue + + + + + + + + + + + + + + + + + + + + + +``` + +## 注意事项 + +1. **无需手动导入**:FontAwesomeIcon 组件已在 `vite.config.ts` 中配置为自动导入 +2. **图标名格式**:使用小写、带连字符的图标名(如 `magnifying-glass`) +3. **完整图标列表**:访问 [FontAwesome 官网](https://fontawesome.com/search?o=r&m=free) 查看所有可用图标 +4. **渐进步骤**:可以逐步迁移,Element Plus 图标和 FontAwesome 可以共存 + +## 优化建议 + +1. **减少包体积**:迁移后可以移除 `@element-plus/icons-vue` 依赖 +2. **统一图标风格**:FontAwesome 图标风格更统一 +3. **更好的性能**:FontAwesome 按需加载,不会加载未使用的图标 + +## 查找图标 + +- [Solid Icons 搜索](https://fontawesome.com/search?o=r&m=free) +- 图标名格式:`fa-solid fa-icon-name` +- 在代码中使用时只需:`icon="icon-name"` diff --git a/Yi.Ai.Vue3/index.html b/Yi.Ai.Vue3/index.html index 02766bc4..f62b1dec 100644 --- a/Yi.Ai.Vue3/index.html +++ b/Yi.Ai.Vue3/index.html @@ -17,6 +17,14 @@ + + + + + + + + diff --git a/Yi.Ai.Vue3/src/components/FontAwesomeIcon/index.ts b/Yi.Ai.Vue3/src/components/FontAwesomeIcon/index.ts new file mode 100644 index 00000000..7e1e0dc6 --- /dev/null +++ b/Yi.Ai.Vue3/src/components/FontAwesomeIcon/index.ts @@ -0,0 +1,3 @@ +export { default as FontAwesomeIcon } from './index.vue'; +export { default as FontAwesomeDemo } from './demo.vue'; +export { getFontAwesomeIcon, iconMapping } from '@/utils/icon-mapping'; diff --git a/Yi.Ai.Vue3/src/components/FontAwesomeIcon/index.vue b/Yi.Ai.Vue3/src/components/FontAwesomeIcon/index.vue new file mode 100644 index 00000000..2bb285bd --- /dev/null +++ b/Yi.Ai.Vue3/src/components/FontAwesomeIcon/index.vue @@ -0,0 +1,33 @@ + + + + + diff --git a/Yi.Ai.Vue3/src/components/MarkedMarkdown/index.vue b/Yi.Ai.Vue3/src/components/MarkedMarkdown/index.vue index b457c6ad..6d0a078b 100644 --- a/Yi.Ai.Vue3/src/components/MarkedMarkdown/index.vue +++ b/Yi.Ai.Vue3/src/components/MarkedMarkdown/index.vue @@ -1,7 +1,7 @@ diff --git a/Yi.Ai.Vue3/src/components/ProductPackage/PackageTab.vue b/Yi.Ai.Vue3/src/components/ProductPackage/PackageTab.vue index 2bf3b79a..100f7ffa 100644 --- a/Yi.Ai.Vue3/src/components/ProductPackage/PackageTab.vue +++ b/Yi.Ai.Vue3/src/components/ProductPackage/PackageTab.vue @@ -1,10 +1,13 @@ diff --git a/Yi.Ai.Vue3/src/components/SupportModelProducts/indexl.vue b/Yi.Ai.Vue3/src/components/SupportModelProducts/indexl.vue index df88ebd9..c73acda0 100644 --- a/Yi.Ai.Vue3/src/components/SupportModelProducts/indexl.vue +++ b/Yi.Ai.Vue3/src/components/SupportModelProducts/indexl.vue @@ -1,4 +1,8 @@ diff --git a/Yi.Ai.Vue3/src/config/index.ts b/Yi.Ai.Vue3/src/config/index.ts index 7d60d8fc..c13f4f1c 100644 --- a/Yi.Ai.Vue3/src/config/index.ts +++ b/Yi.Ai.Vue3/src/config/index.ts @@ -12,4 +12,18 @@ export const COLLAPSE_THRESHOLD: number = 600; export const SIDE_BAR_WIDTH: number = 280; // 路由白名单地址[本地存在的路由 staticRouter.ts 中] -export const ROUTER_WHITE_LIST: string[] = ['/chat', '/chat/conversation', '/chat/image', '/chat/video', '/model-library', '/403', '/404']; +// 包含所有无需登录即可访问的公开路径 +export const ROUTER_WHITE_LIST: string[] = [ + '/chat', + '/chat/conversation', + '/chat/image', + '/chat/video', + '/chat/agent', + '/model-library', + '/products', + '/pay-result', + '/activity/:id', + '/announcement/:id', + '/403', + '/404', +]; diff --git a/Yi.Ai.Vue3/src/config/version.ts b/Yi.Ai.Vue3/src/config/version.ts new file mode 100644 index 00000000..f95b2300 --- /dev/null +++ b/Yi.Ai.Vue3/src/config/version.ts @@ -0,0 +1,61 @@ +/** + * 应用版本配置 + * 集中管理应用版本信息 + * + * ⚠️ 注意:修改此处版本号即可,vite.config.ts 会自动读取 + */ + +// 主版本号 - 修改此处即可同步更新所有地方的版本显示 +export const APP_VERSION = '3.6.0'; + +// 应用名称 +export const APP_NAME = '意心AI'; + +// 完整名称(名称 + 版本) +export const APP_FULL_NAME = `${APP_NAME} ${APP_VERSION}`; + +// 构建信息(由 vite 注入) +declare const __GIT_BRANCH__: string; +declare const __GIT_HASH__: string; +declare const __GIT_DATE__: string; +declare const __BUILD_TIME__: string; + +// 版本信息(由 vite 注入) +declare const __APP_VERSION__: string; +declare const __APP_NAME__: string; + +export interface BuildInfo { + version: string; + name: string; + gitBranch: string; + gitHash: string; + gitDate: string; + buildTime: string; +} + +// 获取完整构建信息 +export function getBuildInfo(): BuildInfo { + return { + version: APP_VERSION, + name: APP_NAME, + gitBranch: typeof __GIT_BRANCH__ !== 'undefined' ? __GIT_BRANCH__ : 'unknown', + gitHash: typeof __GIT_HASH__ !== 'undefined' ? __GIT_HASH__ : 'unknown', + gitDate: typeof __GIT_DATE__ !== 'undefined' ? __GIT_DATE__ : 'unknown', + buildTime: typeof __BUILD_TIME__ !== 'undefined' ? __BUILD_TIME__ : new Date().toISOString(), + }; +} + +// 在控制台输出构建信息 +export function logBuildInfo(): void { + console.log( + `%c ${APP_NAME} ${APP_VERSION} %c Build Info `, + 'background:#35495e; padding: 4px; border-radius: 3px 0 0 3px; color: #fff', + 'background:#41b883; padding: 4px; border-radius: 0 3px 3px 0; color: #fff', + ); + const info = getBuildInfo(); + console.log(`🔹 Version: ${info.version}`); + // console.log(`🔹 Git Branch: ${info.gitBranch}`); + console.log(`🔹 Git Commit: ${info.gitHash}`); + // console.log(`🔹 Commit Date: ${info.gitDate}`); + // console.log(`🔹 Build Time: ${info.buildTime}`); +} diff --git a/Yi.Ai.Vue3/src/layouts/components/ChatAside/index.vue b/Yi.Ai.Vue3/src/layouts/components/ChatAside/index.vue index 083e9496..46671a91 100644 --- a/Yi.Ai.Vue3/src/layouts/components/ChatAside/index.vue +++ b/Yi.Ai.Vue3/src/layouts/components/ChatAside/index.vue @@ -20,12 +20,29 @@ const isCollapsed = computed(() => designStore.isCollapseConversationList); // 判断是否为新建对话状态(没有选中任何会话) const isNewChatState = computed(() => !sessionStore.currentSession); +const isLoading = ref(false); -onMounted(async () => { - await sessionStore.requestSessionList(); - if (conversationsList.value.length > 0 && sessionId.value) { - const currentSessionRes = await get_session(`${sessionId.value}`); - sessionStore.setCurrentSession(currentSessionRes.data); +onMounted(() => { + // 使用 requestIdleCallback 或 setTimeout 延迟加载数据 + // 避免阻塞首屏渲染 + const loadData = async () => { + isLoading.value = true; + try { + await sessionStore.requestSessionList(); + if (conversationsList.value.length > 0 && sessionId.value) { + const currentSessionRes = await get_session(`${sessionId.value}`); + sessionStore.setCurrentSession(currentSessionRes.data); + } + } finally { + isLoading.value = false; + } + }; + + // 优先使用 requestIdleCallback,如果不支持则使用 setTimeout + if (typeof requestIdleCallback !== 'undefined') { + requestIdleCallback(() => loadData(), { timeout: 1000 }); + } else { + setTimeout(loadData, 100); } }); diff --git a/Yi.Ai.Vue3/src/layouts/index.vue b/Yi.Ai.Vue3/src/layouts/index.vue index c37bc699..1a92f92d 100644 --- a/Yi.Ai.Vue3/src/layouts/index.vue +++ b/Yi.Ai.Vue3/src/layouts/index.vue @@ -35,30 +35,6 @@ const layout = computed((): LayoutType | 'mobile' => { // 否则使用全局设置的 layout return designStore.layout; }); - -onMounted(() => { - // 更好的做法是等待所有资源加载 - window.addEventListener('load', () => { - const loader = document.getElementById('yixinai-loader'); - if (loader) { - loader.style.opacity = '0'; - setTimeout(() => { - loader.style.display = 'none'; - }, 500); // 匹配过渡时间 - } - }); - - // 设置超时作为兜底 - setTimeout(() => { - const loader = document.getElementById('yixinai-loader'); - if (loader) { - loader.style.opacity = '0'; - setTimeout(() => { - loader.style.display = 'none'; - }, 500); - } - }, 500); // 最多显示0.5秒 -}); diff --git a/Yi.Ai.Vue3/src/main.ts b/Yi.Ai.Vue3/src/main.ts index 501dcb4f..2030ebdc 100644 --- a/Yi.Ai.Vue3/src/main.ts +++ b/Yi.Ai.Vue3/src/main.ts @@ -1,14 +1,14 @@ // 引入ElementPlus所有图标 import * as ElementPlusIconsVue from '@element-plus/icons-vue'; -import { ElMessage } from 'element-plus'; import { createApp } from 'vue'; import ElementPlusX from 'vue-element-plus-x'; +import 'element-plus/dist/index.css'; import App from './App.vue'; +import { logBuildInfo } from './config/version'; import router from './routers'; import store from './stores'; import './styles/index.scss'; import 'virtual:uno.css'; -import 'element-plus/dist/index.css'; import 'virtual:svg-icons-register'; // 创建 Vue 应用 @@ -16,27 +16,78 @@ const app = createApp(App); // 安装插件 app.use(router); -app.use(ElMessage); +app.use(store); app.use(ElementPlusX); -// 注册图标 +// 注册所有 Element Plus 图标(临时方案,后续迁移到 fontawesome) for (const [key, component] of Object.entries(ElementPlusIconsVue)) { app.component(key, component); } -app.use(store); +// 输出构建信息(使用统一版本配置) +logBuildInfo(); -// 输出构建信息 -console.log( - `%c 意心AI 3.3 %c Build Info `, - 'background:#35495e; padding: 4px; border-radius: 3px 0 0 3px; color: #fff', - 'background:#41b883; padding: 4px; border-radius: 0 3px 3px 0; color: #fff', -); -// console.log(`🔹 Git Branch: ${__GIT_BRANCH__}`); -console.log(`🔹 Git Commit: ${__GIT_HASH__}`); -// console.log(`🔹 Commit Date: ${__GIT_DATE__}`); -// console.log(`🔹 Build Time: ${__BUILD_TIME__}`); +import { nextTick } from 'vue'; // 挂载 Vue 应用 -// mount 完成说明应用初始化完毕,此时手动通知 loading 动画结束 app.mount('#app'); + +/** + * 检查页面是否真正渲染完成 + * 改进策略: + * 1. 等待多个 requestAnimationFrame 确保浏览器完成绘制 + * 2. 检查关键元素是否存在且有实际内容 + * 3. 检查关键 CSS 是否已应用 + * 4. 给予最小展示时间,避免闪烁 + */ +function waitForPageRendered(): Promise { + return new Promise((resolve) => { + const minDisplayTime = 800; // 最小展示时间 800ms,避免闪烁 + const maxWaitTime = 8000; // 最大等待时间 8 秒 + const startTime = Date.now(); + + const checkRender = () => { + const elapsed = Date.now() - startTime; + const appElement = document.getElementById('app'); + + // 检查关键条件 + const hasContent = appElement?.children.length > 0; + const hasVisibleHeight = (appElement?.offsetHeight || 0) > 200; + const hasRouterView = document.querySelector('.layout-container') !== null || + document.querySelector('.el-container') !== null || + document.querySelector('#app > div') !== null; + + const isRendered = hasContent && hasVisibleHeight && hasRouterView; + const isMinTimeMet = elapsed >= minDisplayTime; + const isTimeout = elapsed >= maxWaitTime; + + if ((isRendered && isMinTimeMet) || isTimeout) { + // 再多给一帧时间确保稳定 + requestAnimationFrame(() => { + requestAnimationFrame(() => resolve()); + }); + } else { + requestAnimationFrame(checkRender); + } + }; + + // 等待 Vue 更新和浏览器绘制 + nextTick(() => { + requestAnimationFrame(() => { + setTimeout(checkRender, 100); + }); + }); + }); +} + +// 第一阶段:Vue 应用已挂载 +if (typeof window.__hideAppLoader === 'function') { + window.__hideAppLoader('mounted'); +} + +// 等待页面真正渲染完成后再通知第二阶段 +waitForPageRendered().then(() => { + if (typeof window.__hideAppLoader === 'function') { + window.__hideAppLoader('rendered'); + } +}); diff --git a/Yi.Ai.Vue3/src/pages/test/fontawesome.vue b/Yi.Ai.Vue3/src/pages/test/fontawesome.vue new file mode 100644 index 00000000..0c4521a0 --- /dev/null +++ b/Yi.Ai.Vue3/src/pages/test/fontawesome.vue @@ -0,0 +1,29 @@ + + + + + FontAwesome 图标测试页面 + 如果看到以下图标正常显示,说明 FontAwesome 配置成功! + + + + + diff --git a/Yi.Ai.Vue3/src/routers/index.ts b/Yi.Ai.Vue3/src/routers/index.ts index be40af42..42881c36 100644 --- a/Yi.Ai.Vue3/src/routers/index.ts +++ b/Yi.Ai.Vue3/src/routers/index.ts @@ -5,24 +5,69 @@ import { createRouter, createWebHistory } from 'vue-router'; import { ROUTER_WHITE_LIST } from '@/config'; import { checkPagePermission } from '@/config/permission'; import { errorRouter, layoutRouter, staticRouter } from '@/routers/modules/staticRouter'; -import { useDesignStore, useUserStore } from '@/stores'; +import { useUserStore } from '@/stores'; +import { useDesignStore } from '@/stores/modules/design'; -// 创建页面加载进度条,提升用户体验。 +// 创建页面加载进度条,提升用户体验 const { start, done } = useNProgress(0, { - showSpinner: false, // 不显示旋转器 - trickleSpeed: 200, // 进度条增长速度(毫秒) - minimum: 0.3, // 最小进度值(30%) - easing: 'ease', // 动画缓动函数 - speed: 500, // 动画速度 + showSpinner: false, + trickleSpeed: 200, + minimum: 0.3, + easing: 'ease', + speed: 500, }); + // 创建路由实例 const router = createRouter({ - history: createWebHistory(), // 使用 HTML5 History 模式 - routes: [...layoutRouter, ...staticRouter, ...errorRouter], // 合并所有路由 - strict: false, // 不严格匹配尾部斜杠 - scrollBehavior: () => ({ left: 0, top: 0 }), // 路由切换时滚动到顶部 + history: createWebHistory(), + routes: [...layoutRouter, ...staticRouter, ...errorRouter], + strict: false, + scrollBehavior: () => ({ left: 0, top: 0 }), }); +// 预加载标记,防止重复预加载 +const preloadedComponents = new Set(); + +/** + * 预加载路由组件 + * 提前加载可能访问的路由组件,减少路由切换时的等待时间 + */ +function preloadRouteComponents() { + // 预加载核心路由组件 + const coreRoutes = [ + '/chat/conversation', + '/chat/image', + '/chat/video', + '/chat/agent', + '/console/user', + '/model-library', + ]; + + // 延迟预加载,避免影响首屏加载 + setTimeout(() => { + coreRoutes.forEach(path => { + const route = router.resolve(path); + if (route.matched.length > 0) { + const component = route.matched[route.matched.length - 1].components?.default; + if (typeof component === 'function' && !preloadedComponents.has(component)) { + preloadedComponents.add(component); + // 异步预加载,不阻塞主线程 + requestIdleCallback?.(() => { + (component as () => Promise)().catch(() => {}); + }) || setTimeout(() => { + (component as () => Promise)().catch(() => {}); + }, 100); + } + } + }); + }, 2000); +} + +// 首屏加载完成后开始预加载 +if (typeof window !== 'undefined') { + window.addEventListener('load', preloadRouteComponents); +} + // 路由前置守卫 router.beforeEach( async ( @@ -30,54 +75,67 @@ router.beforeEach( _from: RouteLocationNormalized, next: NavigationGuardNext, ) => { - // 1. 获取状态管理 - const userStore = useUserStore(); - const designStore = useDesignStore(); // 必须在守卫内部调用 - // 2. 设置布局(根据路由meta中的layout配置) - designStore._setLayout(to.meta?.layout || 'default'); - - // 3. 开始显示进度条 + // 1. 开始显示进度条 start(); - // 4. 设置页面标题 + // 2. 设置页面标题 document.title = (to.meta.title as string) || (import.meta.env.VITE_WEB_TITLE as string); - // 3、权限 预留 - // 3、判断是访问登陆页,有Token访问当前页面,token过期访问接口,axios封装则自动跳转登录页面,没有Token重置路由到登陆页。 - // if (to.path.toLocaleLowerCase() === LOGIN_URL) { - // // 有Token访问当前页面 - // if (userStore.token) { - // return next(from.fullPath); - // } - // else { - // ElMessage.error('账号身份已过期,请重新登录'); - // } - // // 没有Token重置路由到登陆页。 - // // resetRouter(); // 预留 - // return next(); - // } - // 4、判断访问页面是否在路由白名单地址[静态路由]中,如果存在直接放行。 + // 3. 设置布局(使用 setTimeout 避免阻塞导航) + const layout = to.meta?.layout || 'default'; + setTimeout(() => { + try { + const designStore = useDesignStore(); + designStore._setLayout(layout); + } catch (e) { + // 忽略 store 初始化错误 + } + }, 0); + + // 4. 检查路由是否存在(404 处理) + // 如果 to.matched 为空且 to.name 不存在,说明路由未匹配 + if (to.matched.length === 0 || (to.matched.length === 1 && to.matched[0].path === '/:pathMatch(.*)*')) { + // 404 路由已定义在 errorRouter 中,这里不需要额外处理 + } + // 5. 白名单检查(跳过权限验证) - if (ROUTER_WHITE_LIST.includes(to.path)) + if (ROUTER_WHITE_LIST.some(path => { + // 支持通配符匹配 + if (path.includes(':')) { + const pattern = path.replace(/:\w+/g, '[^/]+'); + const regex = new RegExp(`^${pattern}$`); + return regex.test(to.path); + } + return path === to.path; + })) { return next(); + } - // 6. Token 检查(用户认证),没有重定向到 login 页面。 - if (!userStore.token) - userStore.logout(); + // 6. 获取用户状态(延迟加载,避免阻塞) + let userStore; + try { + userStore = useUserStore(); + } catch (e) { + // Store 未初始化,允许继续 + return next(); + } - // 7. 页面权限检查 + // 7. Token 检查(用户认证) + if (!userStore.token) { + userStore.clearUserInfo(); + return next({ path: '/', replace: true }); + } + + // 8. 页面权限检查 const userName = userStore.userInfo?.user?.userName; const hasPermission = checkPagePermission(to.path, userName); if (!hasPermission) { - // 用户无权访问该页面,跳转到403页面 ElMessage.warning('您没有权限访问该页面'); - return next('/403'); + return next({ path: '/403', replace: true }); } - // 其余逻辑 预留... - - // 8. 放行路由 + // 9. 放行路由 next(); }, ); diff --git a/Yi.Ai.Vue3/src/routers/modules/staticRouter.ts b/Yi.Ai.Vue3/src/routers/modules/staticRouter.ts index 99d72ee6..89533bb1 100644 --- a/Yi.Ai.Vue3/src/routers/modules/staticRouter.ts +++ b/Yi.Ai.Vue3/src/routers/modules/staticRouter.ts @@ -1,10 +1,22 @@ import type { RouteRecordRaw } from 'vue-router'; +// 预加载辅助函数 +function preloadComponent(importFn: () => Promise) { + return () => { + // 在开发环境下直接返回 + if (import.meta.env.DEV) { + return importFn(); + } + // 生产环境下可以添加缓存逻辑 + return importFn(); + }; +} + // LayoutRouter[布局路由] export const layoutRouter: RouteRecordRaw[] = [ { path: '/', - component: () => import('@/layouts/index.vue'), + component: preloadComponent(() => import('@/layouts/index.vue')), children: [ // 将首页重定向逻辑放在这里 { @@ -17,16 +29,12 @@ export const layoutRouter: RouteRecordRaw[] = [ path: 'chat', name: 'chat', component: () => import('@/pages/chat/index.vue'), + redirect: '/chat/conversation', meta: { title: 'AI应用', icon: 'HomeFilled', }, children: [ - // chat 根路径重定向到 conversation - { - path: '', - redirect: '/chat/conversation', - }, { path: 'conversation', name: 'chatConversation', @@ -140,17 +148,13 @@ export const layoutRouter: RouteRecordRaw[] = [ path: 'console', name: 'console', component: () => import('@/pages/console/index.vue'), + redirect: '/console/user', meta: { title: '意心Ai-控制台', icon: 'Setting', layout: 'default', }, children: [ - // console 根路径重定向到 user - { - path: '', - redirect: '/console/user', - }, { path: 'user', name: 'consoleUser', @@ -244,8 +248,18 @@ export const layoutRouter: RouteRecordRaw[] = [ ], }, ]; -// staticRouter[静态路由] 预留 -export const staticRouter: RouteRecordRaw[] = []; +// staticRouter[静态路由] +export const staticRouter: RouteRecordRaw[] = [ + // FontAwesome 测试页面 + { + path: '/test/fontawesome', + name: 'testFontAwesome', + component: () => import('@/pages/test/fontawesome.vue'), + meta: { + title: 'FontAwesome图标测试', + }, + }, +]; // errorRouter (错误页面路由) export const errorRouter = [ diff --git a/Yi.Ai.Vue3/src/stores/modules/user.ts b/Yi.Ai.Vue3/src/stores/modules/user.ts index 2cd7f5e4..6ffa4406 100644 --- a/Yi.Ai.Vue3/src/stores/modules/user.ts +++ b/Yi.Ai.Vue3/src/stores/modules/user.ts @@ -1,12 +1,10 @@ import { defineStore } from 'pinia'; -import { useRouter } from 'vue-router'; export const useUserStore = defineStore( 'user', () => { const token = ref(); const refreshToken = ref(); - const router = useRouter(); const setToken = (value: string, refreshValue?: string) => { token.value = value; if (refreshValue) { @@ -30,7 +28,8 @@ export const useUserStore = defineStore( // 如果需要调用接口,可以在这里调用 clearToken(); clearUserInfo(); - router.replace({ name: 'chatConversationWithId' }); + // 不在 logout 中进行路由跳转,由调用方决定跳转逻辑 + // 这样可以避免路由守卫中的循环重定向问题 }; // 新增:登录弹框状态 diff --git a/Yi.Ai.Vue3/src/utils/icon-mapping.ts b/Yi.Ai.Vue3/src/utils/icon-mapping.ts new file mode 100644 index 00000000..29face0c --- /dev/null +++ b/Yi.Ai.Vue3/src/utils/icon-mapping.ts @@ -0,0 +1,123 @@ +/** + * Element Plus 图标到 FontAwesome 图标的映射 + * 用于迁移过程中的图标替换 + */ +export const iconMapping: Record = { + // 基础操作 + 'Check': 'check', + 'Close': 'xmark', + 'Delete': 'trash', + 'Edit': 'pen-to-square', + 'Plus': 'plus', + 'Minus': 'minus', + 'Search': 'magnifying-glass', + 'Refresh': 'rotate-right', + 'Loading': 'spinner', + 'Download': 'download', + 'Upload': 'upload', + + // 方向 + 'ArrowLeft': 'arrow-left', + 'ArrowRight': 'arrow-right', + 'ArrowUp': 'arrow-up', + 'ArrowDown': 'arrow-down', + 'ArrowLeftBold': 'arrow-left', + 'ArrowRightBold': 'arrow-right', + 'Expand': 'up-right-and-down-left-from-center', + 'Fold': 'down-left-and-up-right-to-center', + + // 界面 + 'FullScreen': 'expand', + 'View': 'eye', + 'Hide': 'eye-slash', + 'Lock': 'lock', + 'Unlock': 'unlock', + 'User': 'user', + 'Setting': 'gear', + 'Menu': 'bars', + 'MoreFilled': 'ellipsis-vertical', + 'Filter': 'filter', + + // 文件 + 'Document': 'file', + 'Folder': 'folder', + 'Files': 'folder-open', + 'CopyDocument': 'copy', + 'DocumentCopy': 'copy', + 'Picture': 'image', + 'VideoPlay': 'circle-play', + 'Microphone': 'microphone', + + // 状态 + 'CircleCheck': 'circle-check', + 'CircleClose': 'circle-xmark', + 'CircleCloseFilled': 'circle-xmark', + 'SuccessFilled': 'circle-check', + 'WarningFilled': 'triangle-exclamation', + 'InfoFilled': 'circle-info', + 'QuestionFilled': 'circle-question', + + // 功能 + 'Share': 'share-nodes', + 'Star': 'star', + 'Heart': 'heart', + 'Bookmark': 'bookmark', + 'CollectionTag': 'tags', + 'Tag': 'tag', + 'PriceTag': 'tag', + + // 消息 + 'ChatLineRound': 'comment', + 'ChatLineSquare': 'comment', + 'Message': 'envelope', + 'Bell': 'bell', + 'Notification': 'bell', + + // 数据 + 'PieChart': 'chart-pie', + 'TrendCharts': 'chart-line', + 'DataAnalysis': 'chart-simple', + 'List': 'list', + + // 时间 + 'Clock': 'clock', + 'Timer': 'hourglass', + 'Calendar': 'calendar', + + // 购物/支付 + 'ShoppingCart': 'cart-shopping', + 'Coin': 'coins', + 'Wallet': 'wallet', + 'TrophyBase': 'trophy', + + // 开发 + 'Tools': 'screwdriver-wrench', + 'MagicStick': 'wand-magic-sparkles', + 'Monitor': 'desktop', + 'ChromeFilled': 'chrome', + 'ElementPlus': 'code', + + // 安全 + 'Key': 'key', + 'Shield': 'shield', + 'Lock': 'lock', + + // 其他 + 'Box': 'box', + 'Service': 'headset', + 'Camera': 'camera', + 'Postcard': 'address-card', + 'Promotion': 'bullhorn', + 'Reading': 'book-open', + 'ZoomIn': 'magnifying-glass-plus', + 'ZoomOut': 'magnifying-glass-minus', +}; + +/** + * 获取 FontAwesome 图标名称 + * @param elementPlusIcon Element Plus 图标名称 + * @returns FontAwesome 图标名称(不含 fa- 前缀) + */ +export function getFontAwesomeIcon(elementPlusIcon: string): string { + return iconMapping[elementPlusIcon] || elementPlusIcon.toLowerCase(); +} diff --git a/Yi.Ai.Vue3/types/import_meta.d.ts b/Yi.Ai.Vue3/types/import_meta.d.ts index 8f2a798b..c98d612e 100644 --- a/Yi.Ai.Vue3/types/import_meta.d.ts +++ b/Yi.Ai.Vue3/types/import_meta.d.ts @@ -7,6 +7,7 @@ interface ImportMetaEnv { readonly VITE_WEB_BASE_API: string; readonly VITE_API_URL: string; readonly VITE_FILE_UPLOAD_API: string; + readonly VITE_BUILD_COMPRESS: string; readonly VITE_SSO_SEVER_URL: string; readonly VITE_APP_VERSION: string; } diff --git a/Yi.Ai.Vue3/vite.config.ts b/Yi.Ai.Vue3/vite.config.ts index 5aa56f37..b3922c3a 100644 --- a/Yi.Ai.Vue3/vite.config.ts +++ b/Yi.Ai.Vue3/vite.config.ts @@ -1,7 +1,7 @@ import { defineConfig, loadEnv } from "vite"; import path from "path"; import plugins from "./.build/plugins"; - +import { APP_VERSION, APP_NAME } from "./src/config/version"; // https://vite.dev/config/ export default defineConfig((cnf) => { @@ -10,6 +10,11 @@ export default defineConfig((cnf) => { const env = loadEnv(mode, process.cwd()); const { VITE_APP_ENV } = env; return { + // 注入全局常量,供 index.html 和项目代码使用 + define: { + __APP_VERSION__: JSON.stringify(APP_VERSION), + __APP_NAME__: JSON.stringify(APP_NAME), + }, base: VITE_APP_ENV === "production" ? "/" : "/", plugins: plugins(cnf), resolve: { @@ -27,6 +32,124 @@ export default defineConfig((cnf) => { }, }, + // 构建优化配置 + build: { + target: 'es2015', + cssTarget: 'chrome80', + // 代码分割策略 + rollupOptions: { + output: { + // 分包策略 - 更精细的分割以提高加载速度 + manualChunks: (id) => { + // Vue 核心库 + if (id.includes('node_modules/vue/') || id.includes('node_modules/@vue/') || id.includes('node_modules/vue-router/')) { + return 'vue-vendor'; + } + // Pinia 状态管理 + if (id.includes('node_modules/pinia/')) { + return 'pinia'; + } + // Element Plus UI 库 + if (id.includes('node_modules/element-plus/') || id.includes('node_modules/@element-plus/')) { + return 'element-plus'; + } + // Markdown 相关 + if (id.includes('node_modules/unified/') || id.includes('node_modules/remark-') || id.includes('node_modules/rehype-') || id.includes('node_modules/marked/')) { + return 'markdown'; + } + // 工具库 + if (id.includes('node_modules/lodash-es/') || id.includes('node_modules/@vueuse/')) { + return 'utils'; + } + // 代码高亮 + if (id.includes('node_modules/highlight.js/') || id.includes('node_modules/shiki/')) { + return 'highlight'; + } + // 图表库 + if (id.includes('node_modules/echarts/')) { + return 'echarts'; + } + // PDF 处理 + if (id.includes('node_modules/pdfjs-dist/')) { + return 'pdf'; + } + // 其他第三方库 + if (id.includes('node_modules/')) { + return 'vendor'; + } + }, + // 文件命名 + chunkFileNames: 'js/[name]-[hash].js', + entryFileNames: 'js/[name]-[hash].js', + assetFileNames: (assetInfo) => { + const name = assetInfo.name || ''; + if (name.endsWith('.css')) { + return 'css/[name]-[hash][extname]'; + } + if (/\.(png|jpe?g|gif|svg|webp|ico)$/.test(name)) { + return 'images/[name]-[hash][extname]'; + } + if (/\.(woff2?|eot|ttf|otf)$/.test(name)) { + return 'fonts/[name]-[hash][extname]'; + } + return '[ext]/[name]-[hash][extname]'; + }, + }, + }, + // 压缩配置 + minify: 'terser', + terserOptions: { + compress: { + drop_console: VITE_APP_ENV === 'production', + drop_debugger: VITE_APP_ENV === 'production', + pure_funcs: VITE_APP_ENV === 'production' ? ['console.log', 'console.info'] : [], + }, + }, + // chunk 大小警告限制 + chunkSizeWarningLimit: 1000, + // 启用 CSS 代码分割 + cssCodeSplit: true, + // 构建后是否生成 source map + sourcemap: VITE_APP_ENV !== 'production', + }, + + // 性能优化 + optimizeDeps: { + include: [ + 'vue', + 'vue-router', + 'pinia', + 'element-plus', + '@element-plus/icons-vue', + 'lodash-es', + '@vueuse/core', + '@fortawesome/vue-fontawesome', + '@fortawesome/fontawesome-svg-core', + '@fortawesome/free-solid-svg-icons', + ], + // 强制预构建依赖 + force: false, + // 排除不需要优化的依赖 + exclude: [], + }, + + // 预加载设置 + preview: { + // 预览服务配置 + }, + + // 实验性功能 + experimental: { + // 启用渲染内联 CSS(提高首次加载速度) + renderBuiltUrl(filename, { hostType }) { + // 生产环境使用相对路径 + if (hostType === 'js') { + return { runtime: `window.__assetsPath(${JSON.stringify(filename)})` }; + } + return { relative: true }; + }, + }, + server: { port: 17001, open: true,
如果看到以下图标正常显示,说明 FontAwesome 配置成功!