AI助手页面开发

This commit is contained in:
付民康
2025-05-06 14:44:35 +08:00
parent b083c63c3a
commit 3697fcee93
4 changed files with 507 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@@ -0,0 +1,273 @@
<script lang="ts" setup>
import { ref, watchEffect } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { useAccountStore } from '@/stores/user';
import { usePageResize } from '@/utils/hooks/usePageResize';
import { emitEvent } from '@/utils/eventBus';
import setting from '@/setting';
const { copyright } = setting;
defineProps<{ asideDefaultWidth: number; }>();
const emit = defineEmits<{
onSelect: [];
}>();
const userStore = useAccountStore();
const sizeStore = usePageResize();
const router = useRouter();
const route = useRoute();
const defaultSelectKeys = ref<string[]>([]);
/**
* 跳转菜单,转向子应用路由 或 自身路由
*/
const onSelect = (e) => {
const route: { microApp?: string, name: string; login?: boolean; } = JSON.parse(e.el.parentNode.getAttribute('data-route'));
if (route) {
if (route.login && !userStore.isLogin) {
if (sizeStore.isMobile.value) {
emit('onSelect');
}
emitEvent('login');
return;
}
location.href = router.resolve(route).href;
// if (route?.microApp) {
// if (microApp.router.current.has(route?.microApp)) {
// microApp.router.push({
// name: 'micoro-app-homeweb-app',
// path: router.resolve(route).href
// });
// } else {
// router.push(route);
// }
// } else {
// router.push(route);
// }
}
emit('onSelect');
};
const menuItemRouteName = ['home', 'gStar', 'aiCopilot', 'contributed', 'issues', 'merge', 'discussionCreated', 'explore', 'userFollowing', 'userStars'];
watchEffect(() => {
// 回显高亮menuitem
const name = route?.name as string;
if (menuItemRouteName.includes(name)) {
defaultSelectKeys.value = [name];
} else {
const basePath = route.meta.basePath as string;
if (basePath) defaultSelectKeys.value = [basePath];
}
});
</script>
<template>
<div class="side-bar-content">
<d-menu @select="onSelect" :open-keys="['work']" mode="vertical" :default-select-keys="defaultSelectKeys" class="menu"
:key="defaultSelectKeys">
<!-- 首页 -->
<d-menu-item v-for="item in [
{ key: 'home', icon: 'gt-plane-home', activeIcon: 'gt-plane-home-red', label: '首页', route: { name: 'home', microApp: 'micoro-app-homeweb-app' } },
// { key: 'gStar', icon: 'gt-plane-GStar', activeIcon: 'gt-plane-GStar-red', label: 'G-Star', route: { name: 'gStar', microApp: 'micoro-app-homeweb-app' } },
// { key: 'my-project', icon: 'gt-plane-new', activeIcon: 'gt-plane-new-red', label: '资源中心', route: undefined },
// { key: 'aiCopilot', icon: 'gt-plane-ai', activeIcon: 'gt-plane-ai-red', label: 'AI Copilot', route: { name: 'aiCopilot', microApp: 'copilot-app' } },
]" :key="item.key" :data-route="item.route ? JSON.stringify(item.route) : ''">
<template #icon>
<g-icon :name="defaultSelectKeys.some(e => item.key === e) ? item.activeIcon : item.icon"
class="default-icon"></g-icon>
<g-icon :name="item.activeIcon" class="active-icon"></g-icon>
</template>
<span>{{ item.label }}</span>
</d-menu-item>
<!-- 个人工作台 -->
<!-- <hr class="line">-->
<!-- <d-sub-menu title="个人工作台" key="work" class="my-work-platform submenu-item">-->
<!-- <d-menu-item v-for="item in [-->
<!-- { key: 'contributed', icon: 'gt-plane-project', activeIcon: 'gt-plane-project-red', label: '参与的项目', route: { name: 'contributed', login: true } },-->
<!-- { key: 'issues', icon: 'gt-plane-issues', activeIcon: 'gt-plane-issues-red', label: 'Issues', route: { name: 'issues', login: true } },-->
<!-- { key: 'merge', icon: 'gt-plane-pullRequest', activeIcon: 'gt-plane-pullRequest-red', label: 'PR', route: { name: 'merge', login: true } },-->
<!-- { key: 'discussionCreated', icon: 'gt-plane-discussions', activeIcon: 'gt-plane-discussions-red', label: '讨论', route: { name: 'discussionCreated', login: true } },-->
<!-- ]" :key="item.key" :data-route="item.route ? JSON.stringify(item.route) : ''">-->
<!-- <template #icon>-->
<!-- <g-icon :name="defaultSelectKeys.some(e => item.key === e) ? item.activeIcon : item.icon"-->
<!-- class="default-icon"></g-icon>-->
<!-- <g-icon :name="item.activeIcon" class="active-icon"></g-icon>-->
<!-- </template>-->
<!-- <span>{{ item.label }}</span>-->
<!-- </d-menu-item>-->
<!-- </d-sub-menu>-->
<!-- &lt;!&ndash; 发现 &ndash;&gt;-->
<!-- <hr class="line">-->
<!-- <div class="py-[10px] px-[16px] text-[#1D1F3A] leading-[20px]">发现</div>-->
<!-- <d-menu-item v-for="item in [-->
<!-- // { key: 'explore', icon: 'gt-plane-search', activeIcon: 'gt-plane-search-red', label: '探索开源', route: { name: 'explore', microApp: 'micoro-app-homeweb-app' } },-->
<!-- { key: 'userFollowing', icon: 'gt-plane-subscribed', activeIcon: 'gt-plane-subscribed-red', label: '我的关注', route: { name: 'userFollowing', microApp: 'user-center', params: { namespace: userStore.accountInfo.username }, login: true } },-->
<!-- { key: 'userStars', icon: 'gt-plane-star', activeIcon: 'gt-plane-star-red', label: 'Starred', route: { name: 'userStars', microApp: 'user-center', params: { namespace: userStore.accountInfo.username }, login: true } },-->
<!-- ]" :key="item.key" :data-route="item.route ? JSON.stringify(item.route) : ''">-->
<!-- <template #icon>-->
<!-- <g-icon :name="defaultSelectKeys.some(e => item.key === e) ? item.activeIcon : item.icon"-->
<!-- class="default-icon"></g-icon>-->
<!-- <g-icon :name="item.activeIcon" class="active-icon"></g-icon>-->
<!-- </template>-->
<!-- <span>{{ item.label }}</span>-->
<!-- </d-menu-item>-->
<!-- <hr class="line">-->
<!-- <g-link class="menu-item-link" target="_blank" href="https://gitcode.com/Gitcode-offical-team/GitCode-Docs/wiki">-->
<!-- <g-icon name="gt-plane-document" class="default-icon" /><g-icon name="gt-plane-document-red"-->
<!-- class="active-icon" />&nbsp;-->
<!-- <span>帮助文档</span>&nbsp;-->
<!-- <g-icon name="gt-line-open" class="!inline-flex align-middle" />-->
<!-- </g-link>-->
<!-- <g-link class="menu-item-link" target="_blank" href="https://gitcode.com/Gitcode-offical-team/GitCode-Docs/issues">-->
<!-- <g-icon name="gt-plane-feedback" class="default-icon" /><g-icon name="gt-plane-feedback-red"-->
<!-- class="active-icon" />&nbsp;-->
<!-- <span>反馈中心</span>&nbsp;-->
<!-- <g-icon name="gt-line-open" class="!inline-flex align-middle" />-->
<!-- </g-link>-->
</d-menu>
<!-- <hr class="line">-->
<!-- <div class="aside-footer">-->
<!-- <p class="whitespace-nowrap">&copy; {{ copyright.digestAbbr }}</p>-->
<!-- <p class="whitespace-nowrap">-->
<!-- <a href="https://beian.miit.gov.cn/#/Integrated/index" target="_blank">{{ copyright.icp }}</a>-->
<!-- </p>-->
<!-- <p class="whitespace-nowrap mt-[16px]">技术支持:</p>-->
<!-- <p class="whitespace-nowrap">-->
<!-- <a href="https://www.csdn.net/" target="_blank" rel="noopener noreferrer">CSDN</a>、<a-->
<!-- href="https://www.huaweicloud.com/" target="_blank" rel="noopener noreferrer">华为云</a>-->
<!-- </p>-->
<!-- </div>-->
</div>
</template>
<style lang="scss" scoped>
.side-bar-content {
overflow-y: auto;
overflow-x: hidden;
@apply h-[100%] py-[32px] px-[16px] flex flex-col justify-between;
.menu {
background: transparent !important;
@apply border-r-0 flex-1;
:deep(.devui-menu-item-vertical-wrapper:first-of-type) {
margin-top: 0;
}
:deep(.devui-menu-item-vertical-wrapper) {
margin-top: 4px !important;
}
:deep(.devui-menu-item) {
background-color: #fff;
border-radius: 6px;
padding: 10px 16px !important;
white-space: nowrap;
.active-icon {
display: none;
}
}
:deep(.devui-menu-item:hover) {
background: #9699B633;
.active-icon {
display: inline-block;
}
.default-icon {
display: none;
}
}
:deep(.devui-menu-item.devui-menu-item-select) {
background: #9699B633 !important;
}
:deep(.devui-menu-item>span) {
flex: none;
color: #1D1F3A;
font-size: 14px;
}
:deep(.my-work-platform .devui-submenu-title-content ~ .icon) {
//background-color: #E3E3EE;
@apply rounded-[4px] w-[20px] h-[20px] text-center leading-[20px];
}
:deep(.my-work-platform .devui-submenu-title-content ~ .icon:hover) {
background-color: #D1D1E0;
}
.submenu-item :deep(.devui-submenu-title) {
padding: 10px 16px !important;
.devui-submenu-title-content {
margin-left: 0;
}
}
.menu-item-link {
@apply inline-block px-[16px] py-[10px] w-[100%] whitespace-nowrap leading-[20px] mt-[4px] first-of-type:mt-0;
&:hover {
color: #DA203E;
.icon {
color: #DA203E !important;
}
.active-icon {
display: inline-block;
}
.default-icon {
display: none;
}
}
.active-icon {
display: none;
}
}
}
.line {
border-color: #F1F1F8;
@apply ml-[10px] mr-[10px] my-[16px];
}
.aside-footer {
padding: 0px 16px;
color: #9C9DB4;
font-size: 14px;
line-height: 20px;
a {
color: inherit;
}
}
:deep(.devui-menu-vertical ul.devui-submenu div.devui-submenu-title i.icon-chevron-up.is-opened) {
transform: none;
}
:deep(.devui-menu-vertical ul.devui-submenu div.devui-submenu-title i.icon-chevron-up) {
transform: none;
&::before {
transform: rotate(0deg) !important;
display: inline-block;
width: 20px;
height: 20px;
transition: transform var(--devui-animation-duration-slow, .3s) var(--devui-animation-ease-in-out-smooth, cubic-bezier(.645, .045, .355, 1));
}
&.is-opened::before {
transform: rotate(180deg) !important;
}
}
}
</style>

View File

@@ -0,0 +1,212 @@
<template>
<d-layout class="d-layout h-full" :class="{ 'home-background': isHome }">
<d-header class="d-header">
<!-- header -->
<Header @clickMenu="handleClickMenu" :asideSetShow="asideSetShow" ref="refHeader">
<!-- 二级导航左侧内容 -->
<template v-if="$slots.headerLeft && !route?.meta?.hideNav" #info>
<slot name="headerLeft"></slot>
</template>
<!-- 二级导航右侧内容 -->
<template v-if="$slots.headerRight && !route?.meta?.hideNav" #menu>
<slot name="headerRight"></slot>
</template>
<template v-if="$slots.bodyHeader && !route?.meta?.hideNav" #bodyHeader>
<slot name="bodyHeader"></slot>
</template>
<template v-if="$slots.headerBottom && !route?.meta?.hideNav" #headerBottom>
<slot name="headerBottom"></slot>
</template>
</Header>
</d-header>
<!-- 顶栏信息 -->
<div v-if="$slots.contentBanner">
<slot name="contentBanner"></slot>
</div>
<!-- 主体 -->
<d-layout class="flex-col">
<!-- <d-aside class="left-side-bar" v-if="asideSetShow" :style="{ width: mainMarginLeftWidth + 'px' }">-->
<!-- <Aside :asideDefaultWidth="mainMarginLeftWidth"></Aside>-->
<!-- </d-aside>-->
<d-drawer v-model="asideVisible" position="left" lock-scroll style="max-height: 100%; top: 0;"
:style="{ width: '240px', overflowY: 'hidden' }">
<div class="p-[16px] flex justify-between items-center h-[60px]">
<GLink class="g-toolbar-left-logo" href="/" />
<d-icon class="cursor-pointer" name="close" @click="asideVisible = false"></d-icon>
</div>
<Aside :asideDefaultWidth="240" @on-select="handleClickMenu" style="height: calc(100% - 60px);padding-top: 0px;"></Aside>
</d-drawer>
<d-layout :style="{ marginLeft: mainMarginLeftWidth + 'px', transition: 'margin-left ease 0.1s' }">
<d-layout :class="responsiveClass" :style="{ minHeight: `calc(100% - ${footerSize?.height.value || 0}px)` }">
<!-- sidebar -->
<d-aside class="d-aside mr-[32px] pt-24" v-if="route?.meta?.sidebarType">
<slot name="sidebar"></slot>
</d-aside>
<!-- 内容 -->
<d-content>
<slot name="content">
<RouterView />
</slot>
</d-content>
</d-layout>
<!-- footer -->
<!-- <d-footer v-if="!route?.meta?.hiddenFooter" ref="refFooter">
<slot name="footer">
<Footer />
</slot>
</d-footer> -->
</d-layout>
</d-layout>
<!-- 浮动工具栏移动端不显示 -->
<!-- <ToolsFloat v-if="showToolsFloat" />-->
</d-layout>
</template>
<script setup lang="ts">
defineOptions({ name: 'DefaultLayout' });
import { RouterView, useRoute, useRouter } from 'vue-router';
import { computed, ref, watch, watchEffect, onMounted, onUnmounted } from 'vue';
import Header from '@/components/Header/index.vue';
import Footer from '@/components/Footer/index.vue';
import Aside from './aside.vue';
import { useGlobalInfoStore } from '@/stores/Global';
import ToolsFloat from '@/components/ToolsFloat/index.vue';
import { useStorage, useElementSize } from '@vueuse/core';
import { usePageResize } from '@/utils/hooks/usePageResize';
import microApp from '@micro-zoe/micro-app';
defineProps<{ customAside?: boolean; }>();
const router = useRouter();
const solt = defineSlots();
const sizeStore = usePageResize();
const globalStore = useGlobalInfoStore();
const route = useRoute();
const asideVisible = ref(false);
const refFooter = ref(null);
const refHeader: any = ref(null);
const footerSize = useElementSize(refFooter);
const isHome = computed(() => route.name === 'home');
// aside 设置可见性 header有两层有顶栏信息,两层sidebar 都不可见
const asideSetShow = computed(() => {
if (route.meta.asideMenu === 'drawer') return false;
return refHeader.value?.height <= 56 && !solt.contentBanner && !route.meta.sidebarType;
});
const emit = defineEmits<{
clickMenu: [];
}>();
watch(route, () => {
globalStore.setIsNotFound(false); // 切换路由默认不显示404防止一直显示404
}, { deep: true });
const asideDefaultWidth = useStorage('asideDefaultWidth', sizeStore.isMobile.value ? 0 : 240);
const mainMarginLeftWidth = computed(() => {
if (!asideSetShow.value || sizeStore.isMobile.value) {
return 0;
} else {
return asideDefaultWidth.value;
}
});
// 是否展示浮动工具栏(非移动端 || 未设置隐藏)
const showToolsFloat = computed(() => {
return sizeStore.widthType.value !== 'sm' && !route.meta.hiddenToolsFloat;
});
const responsiveClass = computed(() => { // 控制移动和pc布局
let className = 'm-auto max-w-normal px-normal w-full';
let viewportContent = 'width=device-width,initial-scale=0,user-scalable=yes,maximum-scale=1.0';
if (sizeStore.isMobile.value) { // 设备是否为手机
if (route?.meta?.hasMobile) { // 是否有手机端页面
className = 'w-full';
viewportContent = 'width=device-width, initial-scale=1.0';
}
} else {
if (route?.meta?.className) {
className = `${route?.meta?.className as string}`;
}
if (route.name === 'homepage' && globalStore.namespaceType === 1) { // 组织主页样式特殊处理
className = 'm-auto min-w-sm px-normal max-w-[1280px] w-full pt-[12px]';
}
}
document?.querySelector("meta[name='viewport']")?.setAttribute('content', viewportContent);
return className;
});
const handleClickMenu = () => {
if (sizeStore.isMobile.value || !asideSetShow.value) { // 不可见时弹框
asideVisible.value = !asideVisible.value;
} else { // 可见时收起
asideDefaultWidth.value = asideDefaultWidth.value >= 240 ? 0 : 240;
}
};
const outOfSearch = ref(true); // 在search页面隐藏footer
router.afterEach((to, form) => {
outOfSearch.value = to.name !== 'search';
});
microApp.router.afterEach({
'micoro-app-homeweb-app': (to, from) => {
outOfSearch.value = to.pathname !== '/search';
}
});
const checkPage = (data: any) => {
if (data.type === 'checkPage') {
return 'ready';
}
};
onMounted(() => {
microApp.addDataListener('micoro-app-homeweb-app', checkPage, false);
});
onUnmounted(() => {
microApp.removeDataListener('micoro-app-homeweb-app', checkPage);
});
</script>
<style lang="scss" scoped>
.left-side-bar {
position: fixed;
left: 0;
bottom: 0;
top: 56px;
background-color: #fff;
z-index: 10;
border-right: 1px solid #F1F1F8;
overflow-y: auto;
overflow-x: hidden;
transition: width ease 0.1s;
}
.g-toolbar-left-logo {
display: inline-block;
width: 102px;
height: 28px;
background: url(../../assets/imgs/logo/logo.png) no-repeat;
background-size: contain;
vertical-align: middle;
background-position: 0 center;
}
.home-background {
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
//background-image: linear-gradient(to bottom, rgba(255, 255, 255, 0.6), rgba(255, 255, 255, 1)),url('@/assets/imgs/home/首页01_BG_@2x.png');
background-image: url('@/assets/imgs/home/首页02_BG_@2x.png');
background-size: cover;
background-position: center;
z-index: -1;
}
}
</style>

View File

@@ -1,4 +1,5 @@
import DefaultLayout from '@/layouts/DefaultLayout/index.vue';
import AILayout from '@/layouts/AILayout/index.vue';
export default [
{
@@ -107,5 +108,26 @@ export default [
}
}
]
},
{
path: '/ai',
redirect: '/ai/home',
component: AILayout,
children: [
{
path: '/ai/home',
name: 'aihome',
component: () => import('@/views/Jyh/Home/index.vue'),
meta: {
micorApp: ['micoro-app-homeweb-app'],
hasMobile: true,
hiddenFooter: true,
reportTitle: '首页',
className: 'w-full min-w-full',
headerClassName: '',
asideMenu: 'drawer' // 左侧菜单抽屉模式
}
}
]
}
];