feat: 项目加载优化

This commit is contained in:
Gsh
2026-02-01 00:30:44 +08:00
parent 3b6887dc2e
commit 11cbb1b612
29 changed files with 1490 additions and 299 deletions

View File

@@ -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 在客户端正确初始化
},
};
},
};
}

View File

@@ -10,15 +10,21 @@ import Components from 'unplugin-vue-components/vite';
import viteCompression from 'vite-plugin-compression'; import viteCompression from 'vite-plugin-compression';
import envTyped from 'vite-plugin-env-typed'; import envTyped from 'vite-plugin-env-typed';
import fontAwesomePlugin from './fontawesome';
import gitHashPlugin from './git-hash'; import gitHashPlugin from './git-hash';
import preloadPlugin from './preload';
import createSvgIcon from './svg-icon'; import createSvgIcon from './svg-icon';
import versionHtmlPlugin from './version-html';
const root = path.resolve(__dirname, '../../'); const root = path.resolve(__dirname, '../../');
function plugins({ mode, command }: ConfigEnv): PluginOption[] { function plugins({ mode, command }: ConfigEnv): PluginOption[] {
return [ return [
versionHtmlPlugin(), // 最先处理 HTML 版本号
gitHashPlugin(), gitHashPlugin(),
preloadPlugin(),
UnoCSS(), UnoCSS(),
fontAwesomePlugin(),
envTyped({ envTyped({
mode, mode,
envDir: root, envDir: root,
@@ -35,7 +41,18 @@ function plugins({ mode, command }: ConfigEnv): PluginOption[] {
dts: path.join(root, 'types', 'auto-imports.d.ts'), dts: path.join(root, 'types', 'auto-imports.d.ts'),
}), }),
Components({ 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'), dts: path.join(root, 'types', 'components.d.ts'),
}), }),
createSvgIcon(command === 'build'), createSvgIcon(command === 'build'),

View File

@@ -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(`<link rel="modulepreload" href="${href}" crossorigin>`);
} else if (href.endsWith('.css')) {
preloadLinks.push(`<link rel="preload" href="${href}" as="style">`);
}
});
// 将预加载标签插入到 </head> 之前
if (preloadLinks.length > 0) {
return html.replace('</head>', `${preloadLinks.join('\n ')}\n</head>`);
}
return html;
},
};
}

View File

@@ -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}`);
},
};
}

View File

@@ -0,0 +1,133 @@
# FontAwesome 图标迁移指南
## 迁移步骤
### 1. 在组件中使用 FontAwesomeIcon
```vue
<!-- 旧方式Element Plus 图标 -->
<template>
<el-icon>
<Check />
</el-icon>
</template>
<script setup lang="ts">
import { Check } from '@element-plus/icons-vue';
</script>
```
```vue
<!-- 新方式FontAwesome -->
<template>
<FontAwesomeIcon icon="check" />
</template>
<!-- 或带 props -->
<template>
<FontAwesomeIcon icon="check" size="lg" />
<FontAwesomeIcon icon="spinner" spin />
<FontAwesomeIcon icon="magnifying-glass" size="xl" />
</template>
```
### 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
<!-- 迁移前 -->
<template>
<div>
<el-icon><Check /></el-icon>
<el-icon><Close /></el-icon>
<el-icon><Delete /></el-icon>
</div>
</template>
<script setup lang="ts">
import { Check, Close, Delete } from '@element-plus/icons-vue';
</script>
<!-- 迁移后 -->
<template>
<div>
<FontAwesomeIcon icon="check" />
<FontAwesomeIcon icon="xmark" />
<FontAwesomeIcon icon="trash" />
</div>
</template>
<script setup lang="ts">
// 不需要 importFontAwesomeIcon 组件已自动导入
</script>
```
## 注意事项
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"`

View File

@@ -17,6 +17,14 @@
<meta name="viewport" <meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"> content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">
<!-- DNS 预解析和预连接 -->
<link rel="dns-prefetch" href="//api.yourdomain.com">
<link rel="preconnect" href="//api.yourdomain.com" crossorigin>
<!-- 预加载关键资源 -->
<link rel="preload" href="/src/main.ts" as="script" crossorigin>
<link rel="modulepreload" href="/src/main.ts">
<style> <style>
/* 全局样式 */ /* 全局样式 */
@@ -112,16 +120,172 @@
<body> <body>
<!-- 加载动画容器 --> <!-- 加载动画容器 -->
<div id="yixinai-loader" class="loader-container"> <div id="yixinai-loader" class="loader-container">
<div class="loader-title">意心Ai 3.6</div> <div class="loader-title">%APP_FULL_NAME%</div>
<div class="loader-subtitle">海外地址仅首次访问预计加载约10秒无需梯子</div> <div class="loader-subtitle">海外地址仅首次访问预计加载约10秒无需梯子</div>
<div class="loader-logo"> <div class="loader-logo">
<div class="pulse-box"></div> <div class="pulse-box"></div>
</div> </div>
<div class="loader-progress-bar">
<div id="loader-progress" class="loader-progress"></div>
</div>
<div id="loader-text" class="loader-text" style="font-size: 0.875rem; margin-top: 0.5rem; color: #666;">加载中...</div>
</div> </div>
<div id="app"></div> <div id="app"></div>
<script>
// 资源加载进度跟踪 - 增强版
(function() {
const progressBar = document.getElementById('loader-progress');
const loaderText = document.getElementById('loader-text');
const loader = document.getElementById('yixinai-loader');
let progress = 0;
let resourcesLoaded = false;
let vueAppMounted = false;
let appRendered = false;
// 更新进度条
function updateProgress(value, text) {
progress = Math.min(value, 99);
if (progressBar) progressBar.style.width = progress.toFixed(1) + '%';
if (loaderText) loaderText.textContent = text;
}
// 阶段管理
const stages = {
init: { weight: 15, name: '初始化' },
resources: { weight: 35, name: '加载资源' },
scripts: { weight: 25, name: '执行脚本' },
render: { weight: 15, name: '渲染页面' },
complete: { weight: 10, name: '启动应用' }
};
let completedStages = new Set();
let currentStage = 'init';
function calculateProgress() {
let totalProgress = 0;
for (const [key, stage] of Object.entries(stages)) {
if (completedStages.has(key)) {
totalProgress += stage.weight;
} else if (key === currentStage) {
// 当前阶段完成一部分
totalProgress += stage.weight * 0.5;
}
}
return Math.min(totalProgress, 99);
}
// 阶段完成
function completeStage(stageName, nextStage) {
completedStages.add(stageName);
currentStage = nextStage || stageName;
const stage = stages[stageName];
updateProgress(calculateProgress(), stage ? `${stage.name}完成` : '加载中...');
}
// 监听资源加载 - 使用更可靠的方式
const resourceTimings = [];
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
resourceTimings.push(...entries);
// 统计未完成资源
const pendingResources = performance.getEntriesByType('resource')
.filter(r => !r.responseEnd || r.responseEnd === 0).length;
if (pendingResources === 0 && resourceTimings.length > 0) {
completeStage('resources', 'scripts');
} else {
updateProgress(calculateProgress(), `加载资源中... (${resourceTimings.length} 已加载)`);
}
});
try {
observer.observe({ entryTypes: ['resource'] });
} catch (e) {
// 降级处理
}
// 初始进度
let initProgress = 0;
function simulateInitProgress() {
if (initProgress < stages.init.weight) {
initProgress += 1;
updateProgress(initProgress, '正在初始化...');
if (initProgress < stages.init.weight) {
setTimeout(simulateInitProgress, 30);
} else {
completeStage('init', 'resources');
}
}
}
simulateInitProgress();
// 页面资源加载完成
window.addEventListener('load', () => {
completeStage('resources', 'scripts');
resourcesLoaded = true;
// 给脚本执行时间
setTimeout(() => {
completeStage('scripts', 'render');
}, 300);
checkAndHideLoader();
});
// 暴露全局方法供 Vue 应用调用 - 分阶段调用
window.__hideAppLoader = function(stage) {
if (stage === 'mounted') {
vueAppMounted = true;
completeStage('scripts', 'render');
} else if (stage === 'rendered') {
appRendered = true;
completeStage('render', 'complete');
}
checkAndHideLoader();
};
// 检查是否可以隐藏加载动画
function checkAndHideLoader() {
// 需要满足资源加载完成、Vue 挂载、页面渲染完成
if (resourcesLoaded && vueAppMounted && appRendered) {
completeStage('complete', '');
updateProgress(100, '加载完成');
// 确保最小显示时间,避免闪烁
const minDisplayTime = 1000;
const elapsed = Date.now() - startTime;
const remaining = Math.max(0, minDisplayTime - elapsed);
setTimeout(() => {
if (loader) {
loader.style.opacity = '0';
loader.style.transition = 'opacity 0.5s ease';
setTimeout(() => {
loader.remove();
delete window.__hideAppLoader;
}, 500);
}
}, remaining);
}
}
const startTime = Date.now();
// 超时保护:最多显示 30 秒
setTimeout(() => {
if (loader && loader.parentNode) {
console.warn('加载超时,强制隐藏加载动画');
vueAppMounted = true;
resourcesLoaded = true;
appRendered = true;
checkAndHideLoader();
}
}, 30000);
})();
</script>
<script type="module" src="/src/main.ts"></script> <script type="module" src="/src/main.ts"></script>
</body> </body>

View File

@@ -34,6 +34,9 @@
"@floating-ui/core": "^1.7.2", "@floating-ui/core": "^1.7.2",
"@floating-ui/dom": "^1.7.2", "@floating-ui/dom": "^1.7.2",
"@floating-ui/vue": "^1.1.7", "@floating-ui/vue": "^1.1.7",
"@fortawesome/fontawesome-svg-core": "^7.1.0",
"@fortawesome/free-solid-svg-icons": "^7.1.0",
"@fortawesome/vue-fontawesome": "^3.1.3",
"@jsonlee_12138/enum": "^1.0.4", "@jsonlee_12138/enum": "^1.0.4",
"@shikijs/transformers": "^3.7.0", "@shikijs/transformers": "^3.7.0",
"@vueuse/core": "^13.5.0", "@vueuse/core": "^13.5.0",

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,250 @@
<script setup lang="ts">
/**
* FontAwesome 图标演示组件
* 展示不同大小和样式的图标
*/
const commonIcons = [
'check',
'xmark',
'plus',
'minus',
'trash',
'pen-to-square',
'magnifying-glass',
'rotate-right',
'download',
'upload',
'user',
'gear',
'eye',
'eye-slash',
'lock',
'folder',
'file',
'image',
'comment',
'bell',
'heart',
'star',
'clock',
'calendar',
'share-nodes',
];
const sizes = ['xs', 'sm', 'lg', 'xl', '2x'] as const;
</script>
<template>
<div class="font-awesome-demo">
<h2>FontAwesome 图标演示</h2>
<section>
<h3>基础图标</h3>
<div class="icon-grid">
<div v-for="icon in commonIcons" :key="icon" class="icon-item">
<FontAwesomeIcon :icon="icon" />
<span>{{ icon }}</span>
</div>
</div>
</section>
<section>
<h3>不同尺寸</h3>
<div class="size-demo">
<div v-for="size in sizes" :key="size" class="size-item">
<FontAwesomeIcon icon="star" :size="size" />
<span>{{ size }}</span>
</div>
</div>
</section>
<section>
<h3>动画效果</h3>
<div class="animation-demo">
<div class="anim-item">
<FontAwesomeIcon icon="spinner" spin />
<span>spin</span>
</div>
<div class="anim-item">
<FontAwesomeIcon icon="circle-notch" spin />
<span>circle-notch spin</span>
</div>
<div class="anim-item">
<FontAwesomeIcon icon="heart" pulse />
<span>pulse</span>
</div>
</div>
</section>
<section>
<h3>旋转</h3>
<div class="rotation-demo">
<div class="rot-item">
<FontAwesomeIcon icon="arrow-up" :rotation="0" />
<span>0°</span>
</div>
<div class="rot-item">
<FontAwesomeIcon icon="arrow-up" :rotation="90" />
<span>90°</span>
</div>
<div class="rot-item">
<FontAwesomeIcon icon="arrow-up" :rotation="180" />
<span>180°</span>
</div>
<div class="rot-item">
<FontAwesomeIcon icon="arrow-up" :rotation="270" />
<span>270°</span>
</div>
</div>
</section>
<section>
<h3>实际应用示例</h3>
<div class="examples">
<button class="example-btn">
<FontAwesomeIcon icon="magnifying-glass" />
搜索
</button>
<button class="example-btn primary">
<FontAwesomeIcon icon="check" />
确认
</button>
<button class="example-btn danger">
<FontAwesomeIcon icon="trash" />
删除
</button>
<button class="example-btn">
<FontAwesomeIcon icon="download" />
下载
</button>
<button class="example-btn">
<FontAwesomeIcon icon="share-nodes" />
分享
</button>
</div>
</section>
</div>
</template>
<style scoped lang="scss">
.font-awesome-demo {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
h2 {
margin-bottom: 30px;
color: var(--el-text-color-primary);
}
h3 {
margin: 30px 0 15px;
color: var(--el-text-color-regular);
border-bottom: 1px solid var(--el-border-color);
padding-bottom: 10px;
}
section {
margin-bottom: 40px;
}
.icon-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 15px;
.icon-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 15px;
border: 1px solid var(--el-border-color);
border-radius: 8px;
transition: all 0.3s;
&:hover {
border-color: var(--el-color-primary);
background-color: var(--el-color-primary-light-9);
transform: translateY(-2px);
}
span {
margin-top: 8px;
font-size: 12px;
color: var(--el-text-color-secondary);
}
}
}
.size-demo,
.animation-demo,
.rotation-demo {
display: flex;
gap: 30px;
flex-wrap: wrap;
.size-item,
.anim-item,
.rot-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
border: 1px solid var(--el-border-color);
border-radius: 8px;
min-width: 80px;
span {
margin-top: 10px;
font-size: 12px;
color: var(--el-text-color-secondary);
}
}
}
.examples {
display: flex;
gap: 15px;
flex-wrap: wrap;
.example-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
border: 1px solid var(--el-border-color);
border-radius: 6px;
background: var(--el-bg-color);
color: var(--el-text-color-primary);
cursor: pointer;
transition: all 0.3s;
&:hover {
border-color: var(--el-color-primary);
color: var(--el-color-primary);
}
&.primary {
background-color: var(--el-color-primary);
color: white;
border-color: var(--el-color-primary);
&:hover {
background-color: var(--el-color-primary-light-3);
}
}
&.danger {
background-color: var(--el-color-danger);
color: white;
border-color: var(--el-color-danger);
&:hover {
background-color: var(--el-color-danger-light-3);
}
}
}
}
}
</style>

View File

@@ -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';

View File

@@ -0,0 +1,33 @@
<script setup lang="ts">
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
interface Props {
/** FontAwesome 图标名称(不含 fa- 前缀) */
icon: string;
/** 图标大小 */
size?: 'xs' | 'sm' | 'lg' | 'xl' | '2x' | '3x' | '4x' | '5x';
/** 旋转动画 */
spin?: boolean;
/** 脉冲动画 */
pulse?: boolean;
/** 旋转角度 */
rotation?: 0 | 90 | 180 | 270;
}
const props = withDefaults(defineProps<Props>(), {
size: undefined,
spin: false,
pulse: false,
rotation: undefined,
});
</script>
<template>
<FontAwesomeIcon
:icon="`fa-solid fa-${icon}`"
:size="size"
:spin="spin"
:pulse="pulse"
:rotation="rotation"
/>
</template>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, nextTick, onUnmounted, ref, watch } from 'vue'; import { computed, nextTick, onUnmounted, ref, watch } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { Marked } from 'marked'; import { marked } from 'marked';
import hljs from 'highlight.js'; import hljs from 'highlight.js';
import DOMPurify from 'dompurify'; import DOMPurify from 'dompurify';
import { ElDrawer } from 'element-plus'; import { ElDrawer } from 'element-plus';
@@ -27,9 +27,6 @@ const drawerVisible = ref(false);
const previewHtml = ref(''); const previewHtml = ref('');
const iframeRef = ref<HTMLIFrameElement | null>(null); const iframeRef = ref<HTMLIFrameElement | null>(null);
// 创建 marked 实例
const marked = new Marked();
// 配置 marked // 配置 marked
marked.setOptions({ marked.setOptions({
gfm: true, gfm: true,

View File

@@ -8,8 +8,10 @@ import { useModelStore } from '@/stores/modules/model';
import { showProductPackage } from '@/utils/product-package.ts'; import { showProductPackage } from '@/utils/product-package.ts';
import { isUserVip } from '@/utils/user'; import { isUserVip } from '@/utils/user';
import { modelList as localModelList } from './modelData'; import { modelList as localModelList } from './modelData';
import { useRouter } from 'vue-router';
const modelStore = useModelStore(); const modelStore = useModelStore();
const router = useRouter();
const { isMobile } = useResponsive(); const { isMobile } = useResponsive();
const dialogVisible = ref(false); const dialogVisible = ref(false);
const activeTab = ref('provider'); // 'provider' | 'api' const activeTab = ref('provider'); // 'provider' | 'api'
@@ -233,7 +235,7 @@ function handleModelClick(item: GetSessionListVO) {
} }
function goToModelLibrary() { function goToModelLibrary() {
window.location.href = '/model-library'; router.push('/model-library');
} }
/* ------------------------------- /* -------------------------------

View File

@@ -1,9 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import { useRouter } from 'vue-router';
const emit = defineEmits(['close']); const emit = defineEmits(['close']);
const router = useRouter();
function goToActivation() { function goToActivation() {
emit('close'); emit('close');
window.location.href = '/console/activation'; // 使用 router 进行跳转,避免完整页面刷新
setTimeout(() => {
router.push('/console/activation');
}, 300); // 等待对话框关闭动画完成
} }
</script> </script>

View File

@@ -1,10 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { ArrowRight, Box, CircleCheck, Loading, Right, Service } from '@element-plus/icons-vue'; import { ArrowRight, Box, CircleCheck, Loading, Right, Service } from '@element-plus/icons-vue';
import { computed } from 'vue'; import { computed } from 'vue';
import { useRouter } from 'vue-router';
import { promotionConfig } from '@/config/constants.ts'; import { promotionConfig } from '@/config/constants.ts';
import { useUserStore } from '@/stores'; import { useUserStore } from '@/stores';
import { showContactUs } from '@/utils/contact-us.ts'; import { showContactUs } from '@/utils/contact-us.ts';
const router = useRouter();
interface PackageItem { interface PackageItem {
id: number; id: number;
name: string; name: string;
@@ -66,7 +69,7 @@ function contactService() {
} }
function goToModelLibrary() { function goToModelLibrary() {
window.location.href = '/model-library'; router.push('/model-library');
} }
const selectedPackage = computed(() => { const selectedPackage = computed(() => {

View File

@@ -2,6 +2,7 @@
import type { GoodsItem } from '@/api/pay'; import type { GoodsItem } from '@/api/pay';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'; import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { createOrder, getOrderStatus } from '@/api'; import { createOrder, getOrderStatus } from '@/api';
import { getGoodsList, GoodsCategoryType } from '@/api/pay'; import { getGoodsList, GoodsCategoryType } from '@/api/pay';
import ProductPage from '@/pages/products/index.vue'; import ProductPage from '@/pages/products/index.vue';
@@ -11,6 +12,7 @@ import NewbieGuide from './NewbieGuide.vue';
import PackageTab from './PackageTab.vue'; import PackageTab from './PackageTab.vue';
const emit = defineEmits(['close']); const emit = defineEmits(['close']);
const router = useRouter();
// 商品数据类型定义 // 商品数据类型定义
interface PackageItem { interface PackageItem {
@@ -321,8 +323,10 @@ function onClose() {
function goToActivation() { function goToActivation() {
close(); close();
// 使用 window.location 进行跳转,避免 router 注入问题 // 使用 router 进行跳转,避免完整页面刷新
window.location.href = '/console/activation'; setTimeout(() => {
router.push('/console/activation');
}, 300); // 等待对话框关闭动画完成
} }
</script> </script>

View File

@@ -1,4 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { useRouter } from 'vue-router';
const router = useRouter();
const models = [ const models = [
{ name: 'DeepSeek-R1', price: '2', desc: '国产开源,深度思索模式,不过幻读问题比较大,同时具备思考响应链,在开源模型中永远的神!' }, { name: 'DeepSeek-R1', price: '2', desc: '国产开源,深度思索模式,不过幻读问题比较大,同时具备思考响应链,在开源模型中永远的神!' },
{ name: 'DeepSeek-chat', price: '1', desc: '国产开源,简单聊天模式,对于中文文章语义体验较好,但响应速度一般' }, { name: 'DeepSeek-chat', price: '1', desc: '国产开源,简单聊天模式,对于中文文章语义体验较好,但响应速度一般' },
@@ -27,7 +31,7 @@ const models = [
]; ];
function goToModelLibrary() { function goToModelLibrary() {
window.location.href = '/model-library'; router.push('/model-library');
} }
</script> </script>

View File

@@ -12,4 +12,18 @@ export const COLLAPSE_THRESHOLD: number = 600;
export const SIDE_BAR_WIDTH: number = 280; export const SIDE_BAR_WIDTH: number = 280;
// 路由白名单地址[本地存在的路由 staticRouter.ts 中] // 路由白名单地址[本地存在的路由 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',
];

View File

@@ -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}`);
}

View File

@@ -20,13 +20,30 @@ const isCollapsed = computed(() => designStore.isCollapseConversationList);
// 判断是否为新建对话状态(没有选中任何会话) // 判断是否为新建对话状态(没有选中任何会话)
const isNewChatState = computed(() => !sessionStore.currentSession); const isNewChatState = computed(() => !sessionStore.currentSession);
const isLoading = ref(false);
onMounted(async () => { onMounted(() => {
// 使用 requestIdleCallback 或 setTimeout 延迟加载数据
// 避免阻塞首屏渲染
const loadData = async () => {
isLoading.value = true;
try {
await sessionStore.requestSessionList(); await sessionStore.requestSessionList();
if (conversationsList.value.length > 0 && sessionId.value) { if (conversationsList.value.length > 0 && sessionId.value) {
const currentSessionRes = await get_session(`${sessionId.value}`); const currentSessionRes = await get_session(`${sessionId.value}`);
sessionStore.setCurrentSession(currentSessionRes.data); sessionStore.setCurrentSession(currentSessionRes.data);
} }
} finally {
isLoading.value = false;
}
};
// 优先使用 requestIdleCallback如果不支持则使用 setTimeout
if (typeof requestIdleCallback !== 'undefined') {
requestIdleCallback(() => loadData(), { timeout: 1000 });
} else {
setTimeout(loadData, 100);
}
}); });
watch( watch(

View File

@@ -35,30 +35,6 @@ const layout = computed((): LayoutType | 'mobile' => {
// 否则使用全局设置的 layout // 否则使用全局设置的 layout
return designStore.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秒
});
</script> </script>
<template> <template>

View File

@@ -1,14 +1,14 @@
// 引入ElementPlus所有图标 // 引入ElementPlus所有图标
import * as ElementPlusIconsVue from '@element-plus/icons-vue'; import * as ElementPlusIconsVue from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
import { createApp } from 'vue'; import { createApp } from 'vue';
import ElementPlusX from 'vue-element-plus-x'; import ElementPlusX from 'vue-element-plus-x';
import 'element-plus/dist/index.css';
import App from './App.vue'; import App from './App.vue';
import { logBuildInfo } from './config/version';
import router from './routers'; import router from './routers';
import store from './stores'; import store from './stores';
import './styles/index.scss'; import './styles/index.scss';
import 'virtual:uno.css'; import 'virtual:uno.css';
import 'element-plus/dist/index.css';
import 'virtual:svg-icons-register'; import 'virtual:svg-icons-register';
// 创建 Vue 应用 // 创建 Vue 应用
@@ -16,27 +16,78 @@ const app = createApp(App);
// 安装插件 // 安装插件
app.use(router); app.use(router);
app.use(ElMessage); app.use(store);
app.use(ElementPlusX); app.use(ElementPlusX);
// 注册图标 // 注册所有 Element Plus 图标(临时方案,后续迁移到 fontawesome
for (const [key, component] of Object.entries(ElementPlusIconsVue)) { for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component); app.component(key, component);
} }
app.use(store); // 输出构建信息(使用统一版本配置)
logBuildInfo();
// 输出构建信息 import { nextTick } from 'vue';
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__}`);
// 挂载 Vue 应用 // 挂载 Vue 应用
// mount 完成说明应用初始化完毕,此时手动通知 loading 动画结束
app.mount('#app'); app.mount('#app');
/**
* 检查页面是否真正渲染完成
* 改进策略:
* 1. 等待多个 requestAnimationFrame 确保浏览器完成绘制
* 2. 检查关键元素是否存在且有实际内容
* 3. 检查关键 CSS 是否已应用
* 4. 给予最小展示时间,避免闪烁
*/
function waitForPageRendered(): Promise<void> {
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');
}
});

View File

@@ -0,0 +1,29 @@
<script setup lang="ts">
import FontAwesomeDemo from '@/components/FontAwesomeIcon/demo.vue';
</script>
<template>
<div class="fontawesome-test-page">
<h1>FontAwesome 图标测试页面</h1>
<p>如果看到以下图标正常显示说明 FontAwesome 配置成功</p>
<FontAwesomeDemo />
</div>
</template>
<style scoped lang="scss">
.fontawesome-test-page {
padding: 40px;
min-height: 100vh;
background-color: var(--el-bg-color-page);
h1 {
color: var(--el-text-color-primary);
margin-bottom: 10px;
}
p {
color: var(--el-text-color-regular);
margin-bottom: 30px;
}
}
</style>

View File

@@ -5,24 +5,69 @@ import { createRouter, createWebHistory } from 'vue-router';
import { ROUTER_WHITE_LIST } from '@/config'; import { ROUTER_WHITE_LIST } from '@/config';
import { checkPagePermission } from '@/config/permission'; import { checkPagePermission } from '@/config/permission';
import { errorRouter, layoutRouter, staticRouter } from '@/routers/modules/staticRouter'; 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, { const { start, done } = useNProgress(0, {
showSpinner: false, // 不显示旋转器 showSpinner: false,
trickleSpeed: 200, // 进度条增长速度(毫秒) trickleSpeed: 200,
minimum: 0.3, // 最小进度值30% minimum: 0.3,
easing: 'ease', // 动画缓动函数 easing: 'ease',
speed: 500, // 动画速度 speed: 500,
}); });
// 创建路由实例 // 创建路由实例
const router = createRouter({ const router = createRouter({
history: createWebHistory(), // 使用 HTML5 History 模式 history: createWebHistory(),
routes: [...layoutRouter, ...staticRouter, ...errorRouter], // 合并所有路由 routes: [...layoutRouter, ...staticRouter, ...errorRouter],
strict: false, // 不严格匹配尾部斜杠 strict: false,
scrollBehavior: () => ({ left: 0, top: 0 }), // 路由切换时滚动到顶部 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<any>)().catch(() => {});
}) || setTimeout(() => {
(component as () => Promise<any>)().catch(() => {});
}, 100);
}
}
});
}, 2000);
}
// 首屏加载完成后开始预加载
if (typeof window !== 'undefined') {
window.addEventListener('load', preloadRouteComponents);
}
// 路由前置守卫 // 路由前置守卫
router.beforeEach( router.beforeEach(
async ( async (
@@ -30,54 +75,67 @@ router.beforeEach(
_from: RouteLocationNormalized, _from: RouteLocationNormalized,
next: NavigationGuardNext, next: NavigationGuardNext,
) => { ) => {
// 1. 获取状态管理 // 1. 开始显示进度条
const userStore = useUserStore();
const designStore = useDesignStore(); // 必须在守卫内部调用
// 2. 设置布局根据路由meta中的layout配置
designStore._setLayout(to.meta?.layout || 'default');
// 3. 开始显示进度条
start(); start();
// 4. 设置页面标题 // 2. 设置页面标题
document.title = (to.meta.title as string) || (import.meta.env.VITE_WEB_TITLE as string); document.title = (to.meta.title as string) || (import.meta.env.VITE_WEB_TITLE as string);
// 3、权限 预留 // 3. 设置布局(使用 setTimeout 避免阻塞导航)
// 3、判断是访问登陆页有Token访问当前页面token过期访问接口axios封装则自动跳转登录页面没有Token重置路由到登陆页。 const layout = to.meta?.layout || 'default';
// if (to.path.toLocaleLowerCase() === LOGIN_URL) { setTimeout(() => {
// // 有Token访问当前页面 try {
// if (userStore.token) { const designStore = useDesignStore();
// return next(from.fullPath); designStore._setLayout(layout);
// } } catch (e) {
// else { // 忽略 store 初始化错误
// ElMessage.error('账号身份已过期,请重新登录'); }
// } }, 0);
// // 没有Token重置路由到登陆页。
// // resetRouter(); // 预留 // 4. 检查路由是否存在404 处理)
// return next(); // 如果 to.matched 为空且 to.name 不存在,说明路由未匹配
// } if (to.matched.length === 0 || (to.matched.length === 1 && to.matched[0].path === '/:pathMatch(.*)*')) {
// 4、判断访问页面是否在路由白名单地址[静态路由]中,如果存在直接放行。 // 404 路由已定义在 errorRouter 中,这里不需要额外处理
}
// 5. 白名单检查(跳过权限验证) // 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(); return next();
}
// 6. Token 检查(用户认证),没有重定向到 login 页面。 // 6. 获取用户状态(延迟加载,避免阻塞)
if (!userStore.token) let userStore;
userStore.logout(); 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 userName = userStore.userInfo?.user?.userName;
const hasPermission = checkPagePermission(to.path, userName); const hasPermission = checkPagePermission(to.path, userName);
if (!hasPermission) { if (!hasPermission) {
// 用户无权访问该页面跳转到403页面
ElMessage.warning('您没有权限访问该页面'); ElMessage.warning('您没有权限访问该页面');
return next('/403'); return next({ path: '/403', replace: true });
} }
// 其余逻辑 预留... // 9. 放行路由
// 8. 放行路由
next(); next();
}, },
); );

View File

@@ -1,10 +1,22 @@
import type { RouteRecordRaw } from 'vue-router'; import type { RouteRecordRaw } from 'vue-router';
// 预加载辅助函数
function preloadComponent(importFn: () => Promise<any>) {
return () => {
// 在开发环境下直接返回
if (import.meta.env.DEV) {
return importFn();
}
// 生产环境下可以添加缓存逻辑
return importFn();
};
}
// LayoutRouter[布局路由] // LayoutRouter[布局路由]
export const layoutRouter: RouteRecordRaw[] = [ export const layoutRouter: RouteRecordRaw[] = [
{ {
path: '/', path: '/',
component: () => import('@/layouts/index.vue'), component: preloadComponent(() => import('@/layouts/index.vue')),
children: [ children: [
// 将首页重定向逻辑放在这里 // 将首页重定向逻辑放在这里
{ {
@@ -17,16 +29,12 @@ export const layoutRouter: RouteRecordRaw[] = [
path: 'chat', path: 'chat',
name: 'chat', name: 'chat',
component: () => import('@/pages/chat/index.vue'), component: () => import('@/pages/chat/index.vue'),
redirect: '/chat/conversation',
meta: { meta: {
title: 'AI应用', title: 'AI应用',
icon: 'HomeFilled', icon: 'HomeFilled',
}, },
children: [ children: [
// chat 根路径重定向到 conversation
{
path: '',
redirect: '/chat/conversation',
},
{ {
path: 'conversation', path: 'conversation',
name: 'chatConversation', name: 'chatConversation',
@@ -140,17 +148,13 @@ export const layoutRouter: RouteRecordRaw[] = [
path: 'console', path: 'console',
name: 'console', name: 'console',
component: () => import('@/pages/console/index.vue'), component: () => import('@/pages/console/index.vue'),
redirect: '/console/user',
meta: { meta: {
title: '意心Ai-控制台', title: '意心Ai-控制台',
icon: 'Setting', icon: 'Setting',
layout: 'default', layout: 'default',
}, },
children: [ children: [
// console 根路径重定向到 user
{
path: '',
redirect: '/console/user',
},
{ {
path: 'user', path: 'user',
name: 'consoleUser', name: 'consoleUser',
@@ -244,8 +248,18 @@ export const layoutRouter: RouteRecordRaw[] = [
], ],
}, },
]; ];
// staticRouter[静态路由] 预留 // staticRouter[静态路由]
export const staticRouter: RouteRecordRaw[] = []; export const staticRouter: RouteRecordRaw[] = [
// FontAwesome 测试页面
{
path: '/test/fontawesome',
name: 'testFontAwesome',
component: () => import('@/pages/test/fontawesome.vue'),
meta: {
title: 'FontAwesome图标测试',
},
},
];
// errorRouter (错误页面路由) // errorRouter (错误页面路由)
export const errorRouter = [ export const errorRouter = [

View File

@@ -1,12 +1,10 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { useRouter } from 'vue-router';
export const useUserStore = defineStore( export const useUserStore = defineStore(
'user', 'user',
() => { () => {
const token = ref<string>(); const token = ref<string>();
const refreshToken = ref<string | undefined>(); const refreshToken = ref<string | undefined>();
const router = useRouter();
const setToken = (value: string, refreshValue?: string) => { const setToken = (value: string, refreshValue?: string) => {
token.value = value; token.value = value;
if (refreshValue) { if (refreshValue) {
@@ -30,7 +28,8 @@ export const useUserStore = defineStore(
// 如果需要调用接口,可以在这里调用 // 如果需要调用接口,可以在这里调用
clearToken(); clearToken();
clearUserInfo(); clearUserInfo();
router.replace({ name: 'chatConversationWithId' }); // 不在 logout 中进行路由跳转,由调用方决定跳转逻辑
// 这样可以避免路由守卫中的循环重定向问题
}; };
// 新增:登录弹框状态 // 新增:登录弹框状态

View File

@@ -0,0 +1,123 @@
/**
* Element Plus 图标到 FontAwesome 图标的映射
* 用于迁移过程中的图标替换
*/
export const iconMapping: Record<string, string> = {
// 基础操作
'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();
}

View File

@@ -7,6 +7,7 @@ interface ImportMetaEnv {
readonly VITE_WEB_BASE_API: string; readonly VITE_WEB_BASE_API: string;
readonly VITE_API_URL: string; readonly VITE_API_URL: string;
readonly VITE_FILE_UPLOAD_API: string; readonly VITE_FILE_UPLOAD_API: string;
readonly VITE_BUILD_COMPRESS: string;
readonly VITE_SSO_SEVER_URL: string; readonly VITE_SSO_SEVER_URL: string;
readonly VITE_APP_VERSION: string; readonly VITE_APP_VERSION: string;
} }

View File

@@ -1,7 +1,7 @@
import { defineConfig, loadEnv } from "vite"; import { defineConfig, loadEnv } from "vite";
import path from "path"; import path from "path";
import plugins from "./.build/plugins"; import plugins from "./.build/plugins";
import { APP_VERSION, APP_NAME } from "./src/config/version";
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig((cnf) => { export default defineConfig((cnf) => {
@@ -10,6 +10,11 @@ export default defineConfig((cnf) => {
const env = loadEnv(mode, process.cwd()); const env = loadEnv(mode, process.cwd());
const { VITE_APP_ENV } = env; const { VITE_APP_ENV } = env;
return { return {
// 注入全局常量,供 index.html 和项目代码使用
define: {
__APP_VERSION__: JSON.stringify(APP_VERSION),
__APP_NAME__: JSON.stringify(APP_NAME),
},
base: VITE_APP_ENV === "production" ? "/" : "/", base: VITE_APP_ENV === "production" ? "/" : "/",
plugins: plugins(cnf), plugins: plugins(cnf),
resolve: { 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: { server: {
port: 17001, port: 17001,
open: true, open: true,