feat: 项目加载优化
This commit is contained in:
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">
|
||||
import { computed, nextTick, onUnmounted, ref, watch } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { Marked } from 'marked';
|
||||
import { marked } from 'marked';
|
||||
import hljs from 'highlight.js';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { ElDrawer } from 'element-plus';
|
||||
@@ -27,9 +27,6 @@ const drawerVisible = ref(false);
|
||||
const previewHtml = ref('');
|
||||
const iframeRef = ref<HTMLIFrameElement | null>(null);
|
||||
|
||||
// 创建 marked 实例
|
||||
const marked = new Marked();
|
||||
|
||||
// 配置 marked
|
||||
marked.setOptions({
|
||||
gfm: true,
|
||||
|
||||
@@ -8,8 +8,10 @@ import { useModelStore } from '@/stores/modules/model';
|
||||
import { showProductPackage } from '@/utils/product-package.ts';
|
||||
import { isUserVip } from '@/utils/user';
|
||||
import { modelList as localModelList } from './modelData';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const modelStore = useModelStore();
|
||||
const router = useRouter();
|
||||
const { isMobile } = useResponsive();
|
||||
const dialogVisible = ref(false);
|
||||
const activeTab = ref('provider'); // 'provider' | 'api'
|
||||
@@ -233,7 +235,7 @@ function handleModelClick(item: GetSessionListVO) {
|
||||
}
|
||||
|
||||
function goToModelLibrary() {
|
||||
window.location.href = '/model-library';
|
||||
router.push('/model-library');
|
||||
}
|
||||
|
||||
/* -------------------------------
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
const router = useRouter();
|
||||
|
||||
function goToActivation() {
|
||||
emit('close');
|
||||
window.location.href = '/console/activation';
|
||||
// 使用 router 进行跳转,避免完整页面刷新
|
||||
setTimeout(() => {
|
||||
router.push('/console/activation');
|
||||
}, 300); // 等待对话框关闭动画完成
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { ArrowRight, Box, CircleCheck, Loading, Right, Service } from '@element-plus/icons-vue';
|
||||
import { computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { promotionConfig } from '@/config/constants.ts';
|
||||
import { useUserStore } from '@/stores';
|
||||
import { showContactUs } from '@/utils/contact-us.ts';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
interface PackageItem {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -66,7 +69,7 @@ function contactService() {
|
||||
}
|
||||
|
||||
function goToModelLibrary() {
|
||||
window.location.href = '/model-library';
|
||||
router.push('/model-library');
|
||||
}
|
||||
|
||||
const selectedPackage = computed(() => {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import type { GoodsItem } from '@/api/pay';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { createOrder, getOrderStatus } from '@/api';
|
||||
import { getGoodsList, GoodsCategoryType } from '@/api/pay';
|
||||
import ProductPage from '@/pages/products/index.vue';
|
||||
@@ -11,6 +12,7 @@ import NewbieGuide from './NewbieGuide.vue';
|
||||
import PackageTab from './PackageTab.vue';
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
const router = useRouter();
|
||||
|
||||
// 商品数据类型定义
|
||||
interface PackageItem {
|
||||
@@ -321,8 +323,10 @@ function onClose() {
|
||||
|
||||
function goToActivation() {
|
||||
close();
|
||||
// 使用 window.location 进行跳转,避免 router 注入问题
|
||||
window.location.href = '/console/activation';
|
||||
// 使用 router 进行跳转,避免完整页面刷新
|
||||
setTimeout(() => {
|
||||
router.push('/console/activation');
|
||||
}, 300); // 等待对话框关闭动画完成
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const models = [
|
||||
{ name: 'DeepSeek-R1', price: '2', desc: '国产开源,深度思索模式,不过幻读问题比较大,同时具备思考响应链,在开源模型中永远的神!' },
|
||||
{ name: 'DeepSeek-chat', price: '1', desc: '国产开源,简单聊天模式,对于中文文章语义体验较好,但响应速度一般' },
|
||||
@@ -27,7 +31,7 @@ const models = [
|
||||
];
|
||||
|
||||
function goToModelLibrary() {
|
||||
window.location.href = '/model-library';
|
||||
router.push('/model-library');
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -12,4 +12,18 @@ export const COLLAPSE_THRESHOLD: number = 600;
|
||||
export const SIDE_BAR_WIDTH: number = 280;
|
||||
|
||||
// 路由白名单地址[本地存在的路由 staticRouter.ts 中]
|
||||
export const ROUTER_WHITE_LIST: string[] = ['/chat', '/chat/conversation', '/chat/image', '/chat/video', '/model-library', '/403', '/404'];
|
||||
// 包含所有无需登录即可访问的公开路径
|
||||
export const ROUTER_WHITE_LIST: string[] = [
|
||||
'/chat',
|
||||
'/chat/conversation',
|
||||
'/chat/image',
|
||||
'/chat/video',
|
||||
'/chat/agent',
|
||||
'/model-library',
|
||||
'/products',
|
||||
'/pay-result',
|
||||
'/activity/:id',
|
||||
'/announcement/:id',
|
||||
'/403',
|
||||
'/404',
|
||||
];
|
||||
|
||||
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 isLoading = ref(false);
|
||||
|
||||
onMounted(async () => {
|
||||
await sessionStore.requestSessionList();
|
||||
if (conversationsList.value.length > 0 && sessionId.value) {
|
||||
const currentSessionRes = await get_session(`${sessionId.value}`);
|
||||
sessionStore.setCurrentSession(currentSessionRes.data);
|
||||
onMounted(() => {
|
||||
// 使用 requestIdleCallback 或 setTimeout 延迟加载数据
|
||||
// 避免阻塞首屏渲染
|
||||
const loadData = async () => {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
await sessionStore.requestSessionList();
|
||||
if (conversationsList.value.length > 0 && sessionId.value) {
|
||||
const currentSessionRes = await get_session(`${sessionId.value}`);
|
||||
sessionStore.setCurrentSession(currentSessionRes.data);
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 优先使用 requestIdleCallback,如果不支持则使用 setTimeout
|
||||
if (typeof requestIdleCallback !== 'undefined') {
|
||||
requestIdleCallback(() => loadData(), { timeout: 1000 });
|
||||
} else {
|
||||
setTimeout(loadData, 100);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -35,30 +35,6 @@ const layout = computed((): LayoutType | 'mobile' => {
|
||||
// 否则使用全局设置的 layout
|
||||
return designStore.layout;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// 更好的做法是等待所有资源加载
|
||||
window.addEventListener('load', () => {
|
||||
const loader = document.getElementById('yixinai-loader');
|
||||
if (loader) {
|
||||
loader.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
loader.style.display = 'none';
|
||||
}, 500); // 匹配过渡时间
|
||||
}
|
||||
});
|
||||
|
||||
// 设置超时作为兜底
|
||||
setTimeout(() => {
|
||||
const loader = document.getElementById('yixinai-loader');
|
||||
if (loader) {
|
||||
loader.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
loader.style.display = 'none';
|
||||
}, 500);
|
||||
}
|
||||
}, 500); // 最多显示0.5秒
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// 引入ElementPlus所有图标
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { createApp } from 'vue';
|
||||
import ElementPlusX from 'vue-element-plus-x';
|
||||
import 'element-plus/dist/index.css';
|
||||
import App from './App.vue';
|
||||
import { logBuildInfo } from './config/version';
|
||||
import router from './routers';
|
||||
import store from './stores';
|
||||
import './styles/index.scss';
|
||||
import 'virtual:uno.css';
|
||||
import 'element-plus/dist/index.css';
|
||||
import 'virtual:svg-icons-register';
|
||||
|
||||
// 创建 Vue 应用
|
||||
@@ -16,27 +16,78 @@ const app = createApp(App);
|
||||
|
||||
// 安装插件
|
||||
app.use(router);
|
||||
app.use(ElMessage);
|
||||
app.use(store);
|
||||
app.use(ElementPlusX);
|
||||
|
||||
// 注册图标
|
||||
// 注册所有 Element Plus 图标(临时方案,后续迁移到 fontawesome)
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component);
|
||||
}
|
||||
|
||||
app.use(store);
|
||||
// 输出构建信息(使用统一版本配置)
|
||||
logBuildInfo();
|
||||
|
||||
// 输出构建信息
|
||||
console.log(
|
||||
`%c 意心AI 3.3 %c Build Info `,
|
||||
'background:#35495e; padding: 4px; border-radius: 3px 0 0 3px; color: #fff',
|
||||
'background:#41b883; padding: 4px; border-radius: 0 3px 3px 0; color: #fff',
|
||||
);
|
||||
// console.log(`🔹 Git Branch: ${__GIT_BRANCH__}`);
|
||||
console.log(`🔹 Git Commit: ${__GIT_HASH__}`);
|
||||
// console.log(`🔹 Commit Date: ${__GIT_DATE__}`);
|
||||
// console.log(`🔹 Build Time: ${__BUILD_TIME__}`);
|
||||
import { nextTick } from 'vue';
|
||||
|
||||
// 挂载 Vue 应用
|
||||
// mount 完成说明应用初始化完毕,此时手动通知 loading 动画结束
|
||||
app.mount('#app');
|
||||
|
||||
/**
|
||||
* 检查页面是否真正渲染完成
|
||||
* 改进策略:
|
||||
* 1. 等待多个 requestAnimationFrame 确保浏览器完成绘制
|
||||
* 2. 检查关键元素是否存在且有实际内容
|
||||
* 3. 检查关键 CSS 是否已应用
|
||||
* 4. 给予最小展示时间,避免闪烁
|
||||
*/
|
||||
function waitForPageRendered(): Promise<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 { checkPagePermission } from '@/config/permission';
|
||||
import { errorRouter, layoutRouter, staticRouter } from '@/routers/modules/staticRouter';
|
||||
import { useDesignStore, useUserStore } from '@/stores';
|
||||
import { useUserStore } from '@/stores';
|
||||
import { useDesignStore } from '@/stores/modules/design';
|
||||
|
||||
// 创建页面加载进度条,提升用户体验。
|
||||
// 创建页面加载进度条,提升用户体验
|
||||
const { start, done } = useNProgress(0, {
|
||||
showSpinner: false, // 不显示旋转器
|
||||
trickleSpeed: 200, // 进度条增长速度(毫秒)
|
||||
minimum: 0.3, // 最小进度值(30%)
|
||||
easing: 'ease', // 动画缓动函数
|
||||
speed: 500, // 动画速度
|
||||
showSpinner: false,
|
||||
trickleSpeed: 200,
|
||||
minimum: 0.3,
|
||||
easing: 'ease',
|
||||
speed: 500,
|
||||
});
|
||||
|
||||
// 创建路由实例
|
||||
const router = createRouter({
|
||||
history: createWebHistory(), // 使用 HTML5 History 模式
|
||||
routes: [...layoutRouter, ...staticRouter, ...errorRouter], // 合并所有路由
|
||||
strict: false, // 不严格匹配尾部斜杠
|
||||
scrollBehavior: () => ({ left: 0, top: 0 }), // 路由切换时滚动到顶部
|
||||
history: createWebHistory(),
|
||||
routes: [...layoutRouter, ...staticRouter, ...errorRouter],
|
||||
strict: false,
|
||||
scrollBehavior: () => ({ left: 0, top: 0 }),
|
||||
});
|
||||
|
||||
// 预加载标记,防止重复预加载
|
||||
const preloadedComponents = new Set();
|
||||
|
||||
/**
|
||||
* 预加载路由组件
|
||||
* 提前加载可能访问的路由组件,减少路由切换时的等待时间
|
||||
*/
|
||||
function preloadRouteComponents() {
|
||||
// 预加载核心路由组件
|
||||
const coreRoutes = [
|
||||
'/chat/conversation',
|
||||
'/chat/image',
|
||||
'/chat/video',
|
||||
'/chat/agent',
|
||||
'/console/user',
|
||||
'/model-library',
|
||||
];
|
||||
|
||||
// 延迟预加载,避免影响首屏加载
|
||||
setTimeout(() => {
|
||||
coreRoutes.forEach(path => {
|
||||
const route = router.resolve(path);
|
||||
if (route.matched.length > 0) {
|
||||
const component = route.matched[route.matched.length - 1].components?.default;
|
||||
if (typeof component === 'function' && !preloadedComponents.has(component)) {
|
||||
preloadedComponents.add(component);
|
||||
// 异步预加载,不阻塞主线程
|
||||
requestIdleCallback?.(() => {
|
||||
(component as () => Promise<any>)().catch(() => {});
|
||||
}) || setTimeout(() => {
|
||||
(component as () => Promise<any>)().catch(() => {});
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// 首屏加载完成后开始预加载
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('load', preloadRouteComponents);
|
||||
}
|
||||
|
||||
// 路由前置守卫
|
||||
router.beforeEach(
|
||||
async (
|
||||
@@ -30,54 +75,67 @@ router.beforeEach(
|
||||
_from: RouteLocationNormalized,
|
||||
next: NavigationGuardNext,
|
||||
) => {
|
||||
// 1. 获取状态管理
|
||||
const userStore = useUserStore();
|
||||
const designStore = useDesignStore(); // 必须在守卫内部调用
|
||||
// 2. 设置布局(根据路由meta中的layout配置)
|
||||
designStore._setLayout(to.meta?.layout || 'default');
|
||||
|
||||
// 3. 开始显示进度条
|
||||
// 1. 开始显示进度条
|
||||
start();
|
||||
|
||||
// 4. 设置页面标题
|
||||
// 2. 设置页面标题
|
||||
document.title = (to.meta.title as string) || (import.meta.env.VITE_WEB_TITLE as string);
|
||||
|
||||
// 3、权限 预留
|
||||
// 3、判断是访问登陆页,有Token访问当前页面,token过期访问接口,axios封装则自动跳转登录页面,没有Token重置路由到登陆页。
|
||||
// if (to.path.toLocaleLowerCase() === LOGIN_URL) {
|
||||
// // 有Token访问当前页面
|
||||
// if (userStore.token) {
|
||||
// return next(from.fullPath);
|
||||
// }
|
||||
// else {
|
||||
// ElMessage.error('账号身份已过期,请重新登录');
|
||||
// }
|
||||
// // 没有Token重置路由到登陆页。
|
||||
// // resetRouter(); // 预留
|
||||
// return next();
|
||||
// }
|
||||
// 4、判断访问页面是否在路由白名单地址[静态路由]中,如果存在直接放行。
|
||||
// 3. 设置布局(使用 setTimeout 避免阻塞导航)
|
||||
const layout = to.meta?.layout || 'default';
|
||||
setTimeout(() => {
|
||||
try {
|
||||
const designStore = useDesignStore();
|
||||
designStore._setLayout(layout);
|
||||
} catch (e) {
|
||||
// 忽略 store 初始化错误
|
||||
}
|
||||
}, 0);
|
||||
|
||||
// 4. 检查路由是否存在(404 处理)
|
||||
// 如果 to.matched 为空且 to.name 不存在,说明路由未匹配
|
||||
if (to.matched.length === 0 || (to.matched.length === 1 && to.matched[0].path === '/:pathMatch(.*)*')) {
|
||||
// 404 路由已定义在 errorRouter 中,这里不需要额外处理
|
||||
}
|
||||
|
||||
// 5. 白名单检查(跳过权限验证)
|
||||
if (ROUTER_WHITE_LIST.includes(to.path))
|
||||
if (ROUTER_WHITE_LIST.some(path => {
|
||||
// 支持通配符匹配
|
||||
if (path.includes(':')) {
|
||||
const pattern = path.replace(/:\w+/g, '[^/]+');
|
||||
const regex = new RegExp(`^${pattern}$`);
|
||||
return regex.test(to.path);
|
||||
}
|
||||
return path === to.path;
|
||||
})) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// 6. Token 检查(用户认证),没有重定向到 login 页面。
|
||||
if (!userStore.token)
|
||||
userStore.logout();
|
||||
// 6. 获取用户状态(延迟加载,避免阻塞)
|
||||
let userStore;
|
||||
try {
|
||||
userStore = useUserStore();
|
||||
} catch (e) {
|
||||
// Store 未初始化,允许继续
|
||||
return next();
|
||||
}
|
||||
|
||||
// 7. 页面权限检查
|
||||
// 7. Token 检查(用户认证)
|
||||
if (!userStore.token) {
|
||||
userStore.clearUserInfo();
|
||||
return next({ path: '/', replace: true });
|
||||
}
|
||||
|
||||
// 8. 页面权限检查
|
||||
const userName = userStore.userInfo?.user?.userName;
|
||||
const hasPermission = checkPagePermission(to.path, userName);
|
||||
|
||||
if (!hasPermission) {
|
||||
// 用户无权访问该页面,跳转到403页面
|
||||
ElMessage.warning('您没有权限访问该页面');
|
||||
return next('/403');
|
||||
return next({ path: '/403', replace: true });
|
||||
}
|
||||
|
||||
// 其余逻辑 预留...
|
||||
|
||||
// 8. 放行路由
|
||||
// 9. 放行路由
|
||||
next();
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
// 预加载辅助函数
|
||||
function preloadComponent(importFn: () => Promise<any>) {
|
||||
return () => {
|
||||
// 在开发环境下直接返回
|
||||
if (import.meta.env.DEV) {
|
||||
return importFn();
|
||||
}
|
||||
// 生产环境下可以添加缓存逻辑
|
||||
return importFn();
|
||||
};
|
||||
}
|
||||
|
||||
// LayoutRouter[布局路由]
|
||||
export const layoutRouter: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/',
|
||||
component: () => import('@/layouts/index.vue'),
|
||||
component: preloadComponent(() => import('@/layouts/index.vue')),
|
||||
children: [
|
||||
// 将首页重定向逻辑放在这里
|
||||
{
|
||||
@@ -17,16 +29,12 @@ export const layoutRouter: RouteRecordRaw[] = [
|
||||
path: 'chat',
|
||||
name: 'chat',
|
||||
component: () => import('@/pages/chat/index.vue'),
|
||||
redirect: '/chat/conversation',
|
||||
meta: {
|
||||
title: 'AI应用',
|
||||
icon: 'HomeFilled',
|
||||
},
|
||||
children: [
|
||||
// chat 根路径重定向到 conversation
|
||||
{
|
||||
path: '',
|
||||
redirect: '/chat/conversation',
|
||||
},
|
||||
{
|
||||
path: 'conversation',
|
||||
name: 'chatConversation',
|
||||
@@ -140,17 +148,13 @@ export const layoutRouter: RouteRecordRaw[] = [
|
||||
path: 'console',
|
||||
name: 'console',
|
||||
component: () => import('@/pages/console/index.vue'),
|
||||
redirect: '/console/user',
|
||||
meta: {
|
||||
title: '意心Ai-控制台',
|
||||
icon: 'Setting',
|
||||
layout: 'default',
|
||||
},
|
||||
children: [
|
||||
// console 根路径重定向到 user
|
||||
{
|
||||
path: '',
|
||||
redirect: '/console/user',
|
||||
},
|
||||
{
|
||||
path: 'user',
|
||||
name: 'consoleUser',
|
||||
@@ -244,8 +248,18 @@ export const layoutRouter: RouteRecordRaw[] = [
|
||||
],
|
||||
},
|
||||
];
|
||||
// staticRouter[静态路由] 预留
|
||||
export const staticRouter: RouteRecordRaw[] = [];
|
||||
// staticRouter[静态路由]
|
||||
export const staticRouter: RouteRecordRaw[] = [
|
||||
// FontAwesome 测试页面
|
||||
{
|
||||
path: '/test/fontawesome',
|
||||
name: 'testFontAwesome',
|
||||
component: () => import('@/pages/test/fontawesome.vue'),
|
||||
meta: {
|
||||
title: 'FontAwesome图标测试',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// errorRouter (错误页面路由)
|
||||
export const errorRouter = [
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
export const useUserStore = defineStore(
|
||||
'user',
|
||||
() => {
|
||||
const token = ref<string>();
|
||||
const refreshToken = ref<string | undefined>();
|
||||
const router = useRouter();
|
||||
const setToken = (value: string, refreshValue?: string) => {
|
||||
token.value = value;
|
||||
if (refreshValue) {
|
||||
@@ -30,7 +28,8 @@ export const useUserStore = defineStore(
|
||||
// 如果需要调用接口,可以在这里调用
|
||||
clearToken();
|
||||
clearUserInfo();
|
||||
router.replace({ name: 'chatConversationWithId' });
|
||||
// 不在 logout 中进行路由跳转,由调用方决定跳转逻辑
|
||||
// 这样可以避免路由守卫中的循环重定向问题
|
||||
};
|
||||
|
||||
// 新增:登录弹框状态
|
||||
|
||||
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();
|
||||
}
|
||||
Reference in New Issue
Block a user