feat: 项目加载优化
This commit is contained in:
23
Yi.Ai.Vue3/.build/plugins/fontawesome.ts
Normal file
23
Yi.Ai.Vue3/.build/plugins/fontawesome.ts
Normal 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 在客户端正确初始化
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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'),
|
||||||
|
|||||||
47
Yi.Ai.Vue3/.build/plugins/preload.ts
Normal file
47
Yi.Ai.Vue3/.build/plugins/preload.ts
Normal 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;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
20
Yi.Ai.Vue3/.build/plugins/version-html.ts
Normal file
20
Yi.Ai.Vue3/.build/plugins/version-html.ts
Normal 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}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
133
Yi.Ai.Vue3/FONTAWESOME_MIGRATION.md
Normal file
133
Yi.Ai.Vue3/FONTAWESOME_MIGRATION.md
Normal 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">
|
||||||
|
// 不需要 import,FontAwesomeIcon 组件已自动导入
|
||||||
|
</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"`
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
374
Yi.Ai.Vue3/pnpm-lock.yaml
generated
374
Yi.Ai.Vue3/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
250
Yi.Ai.Vue3/src/components/FontAwesomeIcon/demo.vue
Normal file
250
Yi.Ai.Vue3/src/components/FontAwesomeIcon/demo.vue
Normal 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>
|
||||||
3
Yi.Ai.Vue3/src/components/FontAwesomeIcon/index.ts
Normal file
3
Yi.Ai.Vue3/src/components/FontAwesomeIcon/index.ts
Normal 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';
|
||||||
33
Yi.Ai.Vue3/src/components/FontAwesomeIcon/index.vue
Normal file
33
Yi.Ai.Vue3/src/components/FontAwesomeIcon/index.vue
Normal 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>
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------
|
/* -------------------------------
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
|
];
|
||||||
|
|||||||
61
Yi.Ai.Vue3/src/config/version.ts
Normal file
61
Yi.Ai.Vue3/src/config/version.ts
Normal 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}`);
|
||||||
|
}
|
||||||
@@ -20,12 +20,29 @@ const isCollapsed = computed(() => designStore.isCollapseConversationList);
|
|||||||
|
|
||||||
// 判断是否为新建对话状态(没有选中任何会话)
|
// 判断是否为新建对话状态(没有选中任何会话)
|
||||||
const isNewChatState = computed(() => !sessionStore.currentSession);
|
const isNewChatState = computed(() => !sessionStore.currentSession);
|
||||||
|
const isLoading = ref(false);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(() => {
|
||||||
await sessionStore.requestSessionList();
|
// 使用 requestIdleCallback 或 setTimeout 延迟加载数据
|
||||||
if (conversationsList.value.length > 0 && sessionId.value) {
|
// 避免阻塞首屏渲染
|
||||||
const currentSessionRes = await get_session(`${sessionId.value}`);
|
const loadData = async () => {
|
||||||
sessionStore.setCurrentSession(currentSessionRes.data);
|
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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
29
Yi.Ai.Vue3/src/pages/test/fontawesome.vue
Normal file
29
Yi.Ai.Vue3/src/pages/test/fontawesome.vue
Normal 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>
|
||||||
@@ -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();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 = [
|
||||||
|
|||||||
@@ -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 中进行路由跳转,由调用方决定跳转逻辑
|
||||||
|
// 这样可以避免路由守卫中的循环重定向问题
|
||||||
};
|
};
|
||||||
|
|
||||||
// 新增:登录弹框状态
|
// 新增:登录弹框状态
|
||||||
|
|||||||
123
Yi.Ai.Vue3/src/utils/icon-mapping.ts
Normal file
123
Yi.Ai.Vue3/src/utils/icon-mapping.ts
Normal 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();
|
||||||
|
}
|
||||||
1
Yi.Ai.Vue3/types/import_meta.d.ts
vendored
1
Yi.Ai.Vue3/types/import_meta.d.ts
vendored
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user