搜索结果列表页面开发

This commit is contained in:
付民康
2025-03-12 18:41:20 +08:00
commit 7cebe8fc00
739 changed files with 88149 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
<template>
<DefaultLayout>
<template #sidebar>
<h3 class="title">Dashboard</h3>
<NavSidebar :show-avatar="false" />
</template>
</DefaultLayout>
</template>
<script setup lang="ts">
import DefaultLayout from '@/layouts/DefaultLayout/index.vue';
import NavSidebar from '@/components/NavSidebar/index.vue';
</script>
<style scoped lang="scss">
.title{
font-size: 20px;
font-weight: 500;
color: #2D2D2E;
line-height: 32px;
margin-bottom: 24px;
}
</style>

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,195 @@
<template>
<d-layout class="d-layout h-full">
<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);
// 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;
}
</style>

View File

@@ -0,0 +1,30 @@
<template>
<d-layout class="dlayout">
<d-header class="dheader">
<Header />
</d-header>
<d-layout class="dlayout-box">
<d-content class="main-content m-auto max-w-normal min-w-sm px-normal w-full">
<RouterView />
</d-content>
</d-layout>
<d-footer class="dfooter"><Footer></Footer></d-footer>
</d-layout>
</template>
<script setup lang="ts">
import Header from '@/components/Header/index.vue';
import Footer from '@/components/Footer/index.vue';
import { RouterView } from 'vue-router';
</script>
<style scoped>
.dlayout,
.dlayout-box {
min-height: 100%;
}
.dcontent {
flex: 1;
}
</style>

View File

@@ -0,0 +1,193 @@
<template>
<DefaultLayout class="org-layout">
<template v-if="!isMobile && hasPermission" #headerLeft>
<InfoLeft :imgUrl="orgStore.orgInfo?.avatar" :infoText="orgStore.orgInfo?.name" :isFollow="orgStore.isFollow"
@followClick="followClickEvent" :infoSubText="`@${orgStore.orgInfo?.full_path}`" />
</template>
<template v-if="!isMobile && hasPermission" #headerRight>
<div class="pl-[32px] w-full h-full">
<NavTabs :filter="filterFn" />
</div>
</template>
<template #headerBottom v-if="tabPath && !isMobile">
<OrgHeaderInfo v-if="hasPermission" :hideAvatar="false"> </OrgHeaderInfo>
</template>
<template #bodyHeader v-if="isMobile && hasPermission">
<div class="w-full">
<InfoLeft :imgUrl="orgStore.orgInfo?.avatar" :infoText="orgStore.orgInfo?.name"
:infoSubText="`@${orgStore.orgInfo?.full_path}`" :isFollow="orgStore.isFollow"
@followClick="followClickEvent" />
<NavTabs :filter="filterFn" />
</div>
</template>
<template #content>
<slot v-if="hasPermission && !globalInfo.isNotFound" name="content"></slot>
<ErrorPage :code="globalInfo.isNotFound ? 404 : 403" v-else />
</template>
<template v-if="hasPermission" #sidebar>
<NavSidebar :showAvatar="true" :avatarParam="{
avatar: orgStore.orgInfo.avatar,
name: orgStore.orgInfo.name,
infoSubText: `@${orgStore.orgInfo.path}`
}" />
</template>
</DefaultLayout>
</template>
<script setup lang="ts">
import OrgHeaderInfo from '@/views/Org/components/OrgHeaderInfo.vue';
import { onMounted, reactive, ref, computed, watch } from 'vue';
import DefaultLayout from '@/layouts/DefaultLayout/index.vue';
import NavTabs from '@/components/NavTabs/index.vue';
import NavSidebar from '@/components/NavSidebar/index.vue';
import InfoLeft from '@/components/InfoLeft/index.vue';
import ErrorPage from '@/views/ErrorPage/index.vue';
import { orgInfoStore } from '@/stores/Org';
import { getOrgInfo } from '@/views/Org/hooks/orgInfo';
import { useAccountStore } from '@/stores/user';
import setting from '@/setting';
import { usePagePermission } from '@/layouts/hooks/usePagePermission';
import { emitEvent } from '@/utils/eventBus';
import { useGlobalInfoStore } from '@/stores/Global';
import { reqCatch } from '@/utils/catch';
import * as types from '@/api/org/types';
import { getCommunityInfo } from '@/api/org/index';
import { useRoute, useRouter } from 'vue-router';
import { usePageResize } from '@/utils/hooks/usePageResize';
const { isMobile } = usePageResize();
const user = useAccountStore();
const orgStore = orgInfoStore();
const { namespace, followDevCommunity, checkHasFollowedFun, getDevAdvertisement, getDevLogin, getArticleAndFans } = getOrgInfo();
const { hasPermission } = usePagePermission(orgStore.access_level, orgStore.isPrivate);
const globalInfo = useGlobalInfoStore();
globalInfo.updateMenuNum('orgRepos', orgStore.orgInfo.project_count);
// globalInfo.updateMenuNum('orgMember', orgStore.orgInfo.member_count);
const router = useRouter();
const tabPath = computed(() => {
const ary = router.currentRoute.value.path.split('/');
let discussDetail = true;
const disIndex = ary.findIndex((item) => item === 'discussion');
if (disIndex > 0 && disIndex < ary.length - 1) {
discussDetail = false;
}
return !ary.find((item) => item === 'setting') && discussDetail;
});
// navTabs卡片过滤
const filterObj: {
[x: string]: any;
} = {
讨论: 'DISCUSSION',
社区: 'COMMUNITY'
};
const filterFn: any = ref((item: any) => {
return true;
});
const communityHome = ref('');
// 监听组织路由变化
const { accountInfo } = useAccountStore();
// 社区基本信息
let lastNameSpace = '';
const getCommunityInfoFun = async () => {
if (lastNameSpace !== namespace.value) {
const params: types.commonGroupReqType = {
orgId: namespace.value
};
const { data, error } = await reqCatch(getCommunityInfo, params);
if (!error && data) {
orgStore.setCommunityUrl({
communityUrl: `https://${data.data.custom_domain}`,
communityactivityUrl: `https://${data.data.custom_domain}/activelist`
});
communityHome.value = data.data.custom_domain ? `//${data.data.custom_domain}` : '';
orgStore.setCommunityInfo(data.data);
// 获取dev社区登陆状态
getDevLogin();
// 获取社区关注状态
if (user.isLogin) {
checkHasFollowedFun();
}
// 获取社区广告
getDevAdvertisement();
// 获取dev社区文章粉丝数量
getArticleAndFans();
}
}
lastNameSpace = namespace.value;
};
// 监听module_setting权限控制权限变化项目切换
watch(
() => [orgStore.orgInfo?.module_setting?.modules, namespace],
() => {
getCommunityInfoFun();
// const obj = orgStore.orgInfo?.module_setting?.modules.find(mod => mod.key === 'COMMUNITY');
// if (obj && obj.value === '1') getCommunityInfoFun();// 社区开启
filterFn.value = (item: any) => {
// 判断用户权限
const isAllowed = !item.permission || orgStore.access_level >= setting.role[item.permission];
// 判断模块是否打开(讨论,社区等)
// const param = orgStore.orgInfo?.module_setting?.modules.find(mod => mod.key === filterObj[item.label]);
// const isEnabled = !filterObj[item.label] || !!(param && (param as any).value === '1');
// return isEnabled && isAllowed;
return isAllowed;
};
},
{
deep: true,
flush: 'post',
immediate: true
}
);
// 关注点击
const followClickEvent = (val: boolean) => {
if (!user.isLogin) {
emitEvent('login', { triggerType: '关注组织' });
return;
}
followDevCommunity(val);
};
</script>
<style scoped lang="scss">
.back-idex {
display: block;
height: 68px;
padding: 12px 32px;
font-size: 16px;
line-height: 44px;
font-weight: bold;
background-color: #fff;
border-radius: 2px;
cursor: pointer;
margin-bottom: 10px;
white-space: nowrap;
overflow: hidden;
&::before {
content: ' ';
display: inline-block;
width: 24px;
height: 24px;
border-radius: 50%;
border: 1px solid #c7c9d9;
vertical-align: top;
margin-top: 10px;
margin-right: 10px;
background: url('@/assets/imgs/icon/ic-back@2x.png') center no-repeat;
background-size: contain;
}
}
.org-layout {
:deep(.g-header-body-right) {
flex: 1;
width: 0;
}
}
</style>

View File

@@ -0,0 +1,187 @@
<template>
<DefaultLayout class="g-repo-layout">
<template #contentBanner v-if="hasPermission">
<div class="g-repo-header">
<ContentLayout>
<div class="pt-8 g-repo-dashboard-header">
<div class="g-repo-dashboard-width">
<RepoHeaderInfo :widthType="widthType" />
</div>
</div>
</ContentLayout>
</div>
<div class="g-archived" v-if="isArchived">此项目已归档</div>
</template>
<template #content>
<ErrorPage :code="errorCode" v-if="errorCode" />
</template>
<template v-if="hasPermission" #sidebar>
<NavSidebar :key="route.fullPath" :showAvatar="false" />
</template>
</DefaultLayout>
</template>
<script setup lang="ts">
import { ref, computed, watch, onUnmounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { getSettingsValues } from '@/api/repo';
import { reqCatch } from '@/utils/catch';
import { useRepoId } from '@/utils/hooks/useRepoId';
import { repoInfoStore } from '@/stores/Repo';
import { useGlobalInfoStore } from '@/stores/Global';
import { storeToRefs } from 'pinia';
import DefaultLayout from '@/layouts/DefaultLayout/index.vue';
import NavSidebar from '@/components/NavSidebar/index.vue';
import { addEventListener, offEvent } from '@/utils/eventBus';
import { getOrgInfo } from '@/views/Org/hooks/orgInfo';
import * as types from '@/api/org/types';
import { getCommunityInfo } from '@/api/org/index';
import { orgInfoStore } from '@/stores/Org/index';
import setting from '@/setting';
import ErrorPage from '@/views/ErrorPage/index.vue';
import { usePagePermission } from '@/layouts/hooks/usePagePermission';
import RepoHeaderInfo from '@/components/Repo/RepoHeaderInfo/index.vue';
import ContentLayout from '@/components/ContentLayout/index.vue';
import { usePageResize } from '@/utils/hooks/usePageResize';
const { repoId } = useRepoId();
const { widthType } = usePageResize();
const { access_level, isPrivate, repoInfo, isArchived } = storeToRefs(repoInfoStore()); // pinia直接解构会失去响应式
const { hasPermission } = usePagePermission(access_level.value, isPrivate.value);
const globalInfo = useGlobalInfoStore();
globalInfo.updateMenuNum('repoIssues', repoInfo.value.open_issues_count);
globalInfo.updateMenuNum('repoMerge', repoInfo.value.open_merge_requests_count);
globalInfo.updateMenuNum('repoMember', repoInfo.value.member_count);
// 根据权限及是否允许显示设置错误页面
const errorCode = computed(() => {
if (globalInfo.isNotFound) {
return 404;
}
if (!hasPermission.value) {
return 403;
}
return null;
})
// 获取组织namespace
const { namespace } = getOrgInfo();
const route = useRoute();
// 获取仓库
const orgStore = orgInfoStore();
// 社区基本信息
const getCommunityInfoFun = async() => {
const params: types.commonGroupReqType = {
orgId: namespace.value
};
const { data, error } = await reqCatch(getCommunityInfo, params);
if (!error && data) {
orgStore.setCommunityUrl({
communityUrl: `https://${data.data.custom_domain}`,
communityactivityUrl: `https://${data.data.custom_domain}/activelist`
});
orgStore.setCommunityInfo(data.data);
}
};
const filterFn = ref<Function | null>(null);
const nameMap = new Map([
['WIKI', 'repoWiki'],
['ISSUE', 'repoIssues'],
['MERGE_REQUEST', 'repoMerge'],
['FORK', 'repoFork'],
['ANALYSIS', 'repoAnalysis'],
['DISCUSSION', 'repoDiscussion'],
['COMMUNITY', 'reqCommunity']
]);
async function getModuleState() {
const res = await getSettingsValues({
repoId: repoId.value,
group_id: namespace.value
});
if (res.data) {
const { modules } = res.data.data;
const vModules = new Set();
modules.forEach(({ key, value }) => {
// 判断是否获取社区基本信息,拿到社区跳转链接
if (key === 'COMMUNITY' && value === '1') {
getCommunityInfoFun();
}
if (nameMap.get(key) && value === '0') vModules.add(nameMap.get(key));
});
filterFn.value = (item: any) => {
const isAllowed = !item.permission || access_level.value >= setting.role[item.permission];
if (item.id === 'reqCommunity') {
const par = modules.find((item: any) => item.key === 'COMMUNITY');
return !!(par && par.value === '1');
}
const isEnabled = !vModules.has(item.id);
return isEnabled && isAllowed;
};
}
}
// @TODO 此处只用于repo切换时读取项目模块配置信息。是否直接从repo详情获取
watch(repoId, () => getModuleState(), { flush: 'post', immediate: true });
addEventListener('updateNaviBar', getModuleState);
onUnmounted(() => offEvent('updateNaviBar', getModuleState));
</script>
<style scoped lang="scss">
.breadcrumb {
:deep(.g-link) {
font-weight: 500;
color: var(--color-CG600);
line-height: 20px;
&:last-child {
color: var(--color-G900);
}
}
&-splitter {
font-weight: 500;
color: var(--color-CG600);
line-height: 20px;
padding: 0 2px;
}
}
.g-archived {
width: 100%;
padding: 8px;
background-color: var(--color-Y100);
font-weight: bold;
color: var(--color-G900);
line-height: 20px;
text-align: center;
border-bottom: 1px solid var(--color-Y200);
}
.g-repo-header {
background-color: #F9F9FB;
border-bottom: 1px solid #F1F1F8;
position: relative;
z-index: 1;
}
.g-repo-content {
.g-repo-content-width {
max-width: 1536px;
margin: 0 auto;
}
.g-repo-content-header {
border-bottom: 1px solid #f1f1f8;
background: #f9f9fb;
}
}
.g-repo-layout {
:deep(.devui-layout) {
background-color: #fff;
}
:deep(.devui-layout__content) {
background: #fff;
}
}
</style>

View File

@@ -0,0 +1,125 @@
<template>
<DefaultLayout>
<template v-if="!route?.meta?.hideNav && !isSelf && !isMe && !isMobile" #headerLeft>
<InfoLeft :imgUrl="accountInfo?.avatarUrl" :infoText="accountInfo?.name" :isFollow="accountInfo.isFollow"
@followClick="followClickEvent" :isRound="true" userDashBoard/>
</template>
<template v-if="!route?.meta?.hideNav && !isMobile" #headerRight>
<NavTabs :filter="filterFn" />
</template>
<template #bodyHeader v-if="isMobile">
<div class="w-full h-full">
<InfoLeft
v-if="!isSelf && !isMe"
:imgUrl="accountInfo?.avatarUrl"
:infoText="accountInfo?.name"
:isFollow="accountInfo.isFollow"
@followClick="followClickEvent"
:isRound="true"
/>
<NavTabs :filter="filterFn" />
</div>
</template>
<template #content v-if="$slots.content">
<slot name="content"></slot>
</template>
<template #sidebar>
<NavSidebar
isRound
:avatarParam="{
avatar: user.accountInfo.avatar,
name: user.accountInfo.nickname,
infoSubText: `@${user.accountInfo.username}`
}"
>
<template #sidebarInfo>
<div>
<span></span>
</div>
</template>
</NavSidebar>
</template>
</DefaultLayout>
</template>
<script setup lang="ts">
import { computed, watch, onMounted, onBeforeMount } from 'vue';
import { useRoute } from 'vue-router';
import DefaultLayout from '@/layouts/DefaultLayout/index.vue';
import NavTabs from '@/components/NavTabs/index.vue';
import NavSidebar from '@/components/v2/NavSidebar/index.vue';
import InfoLeft from '@/components/InfoLeft/index.vue';
import { useUserInfo } from '@/views/User/hooks/useUserInfo';
import { useAccountStore, otherAccountStore } from '@/stores/user';
import { useStarFollow } from '@/views/User/hooks/useStarFollow';
import { usePageResize } from '@/utils/hooks/usePageResize';
import { getAtomgitIdentity } from '@/api/user';
import { addEventListener, offEvent } from '@/utils/eventBus';
import { useAccount } from '@/utils/hooks/useAccount';
import debounce from 'lodash/debounce';
const { saveAtomgitUserName } = useAccountStore();
const { isMobile } = usePageResize();
const user = useAccountStore();
const other = otherAccountStore();
const { accountInfo } = other;
const { isSelf, userInfo, namespace } = useUserInfo();
const route = useRoute();
const { RecordInfo } = useAccount();
const filterFn = computed(() => (x) => {
if (!accountInfo?.profile) return false;
if (!isSelf && accountInfo?.profile?.setting_private) {
if (x.id === 'userStars') return false;
return true;
}
return true;
});
const isMe = computed(() => {
return other.accountInfo.username === user.accountInfo.username;
});
const { hasFollowed, toggleStar } = useStarFollow(
{
namespace,
username: userInfo.username
},
false
);
watch(
() => hasFollowed.value,
(val) => {
other.saveFollowed(val);
}
);
watch(
() => accountInfo.isFollow,
(val) => {
other.saveFollowed(val);
}
);
const followClickEvent = (item: any) => {
toggleStar(userInfo.username || '', accountInfo.username || '', !accountInfo.isFollow);
};
const onShowPipeLine = async() => {
const res = await getAtomgitIdentity();
if (res.data) {
saveAtomgitUserName(res.data?.data?.extern_username);
}
};
if (route.meta.sidebarType === 'userSetting') {
onShowPipeLine();
}
onMounted(() => {
addEventListener('updateUserInfo', debounce((payload: Record<string, any>) => {
RecordInfo(payload as any);
}, 500));
});
onBeforeMount(() => {
offEvent('updateUserInfo');
});
</script>
<style scoped></style>

View File

@@ -0,0 +1,28 @@
import { ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import setting from '@/setting';
/**
* 判断页面权限
* @param access_level
* @param isPrivate
* @returns
*/
export function usePagePermission(access_level: number = 0, isPrivate: boolean) {
const route = useRoute();
const hasPermission = ref(true);
const permission = route.meta?.permission as string;
// 判断页面访问权限
if (permission) {
hasPermission.value = access_level >= setting.role[permission];
} else {
if (isPrivate) { // 访问私有项目至少是成员
hasPermission.value = access_level >= setting.role['visitor'];
}
}
return {
hasPermission
};
};

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>
<!-- 发现 -->
<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,199 @@
<template>
<d-layout class="d-layout h-full">
<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 pr-[32px] pt-32 g-side-border" v-if="route?.meta?.sidebarType">
<slot name="sidebar"></slot>
</d-aside>
<!-- 内容 -->
<d-content class="pl-[32px]">
<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);
// 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;
}
.g-side-border {
border-right: 1px solid var(--devui-dividing-line);
}
</style>

View File

@@ -0,0 +1,187 @@
<template>
<DefaultLayout class="g-repo-layout">
<template #contentBanner v-if="hasPermission">
<div class="g-repo-header">
<ContentLayout>
<div class="pt-8 g-repo-dashboard-header">
<div class="g-repo-dashboard-width">
<RepoHeaderInfo :widthType="widthType" />
</div>
</div>
</ContentLayout>
</div>
<div class="g-archived" v-if="isArchived">此项目已归档</div>
</template>
<template #content>
<ErrorPage :code="errorCode" v-if="errorCode" />
</template>
<template v-if="hasPermission" #sidebar>
<NavSidebar :key="route.fullPath" :showAvatar="false" />
</template>
</DefaultLayout>
</template>
<script setup lang="ts">
import { ref, computed, watch, onUnmounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { getSettingsValues } from '@/api/repo';
import { reqCatch } from '@/utils/catch';
import { useRepoId } from '@/utils/hooks/useRepoId';
import { repoInfoStore } from '@/stores/Repo';
import { useGlobalInfoStore } from '@/stores/Global';
import { storeToRefs } from 'pinia';
import DefaultLayout from '@/layouts/v2/DefaultLayout/index.vue';
import NavSidebar from '@/components/v2/NavSidebar/index.vue';
import { addEventListener, offEvent } from '@/utils/eventBus';
import { getOrgInfo } from '@/views/Org/hooks/orgInfo';
import * as types from '@/api/org/types';
import { getCommunityInfo } from '@/api/org/index';
import { orgInfoStore } from '@/stores/Org/index';
import setting from '@/setting';
import ErrorPage from '@/views/ErrorPage/index.vue';
import { usePagePermission } from '@/layouts/hooks/usePagePermission';
import RepoHeaderInfo from '@/components/Repo/RepoHeaderInfo/index.vue';
import ContentLayout from '@/components/ContentLayout/index.vue';
import { usePageResize } from '@/utils/hooks/usePageResize';
const { repoId } = useRepoId();
const { widthType } = usePageResize();
const { access_level, isPrivate, repoInfo, isArchived } = storeToRefs(repoInfoStore()); // pinia直接解构会失去响应式
const { hasPermission } = usePagePermission(access_level.value, isPrivate.value);
const globalInfo = useGlobalInfoStore();
globalInfo.updateMenuNum('repoIssues', repoInfo.value.open_issues_count);
globalInfo.updateMenuNum('repoMerge', repoInfo.value.open_merge_requests_count);
globalInfo.updateMenuNum('repoMember', repoInfo.value.member_count);
// 根据权限及是否允许显示设置错误页面
const errorCode = computed(() => {
if (globalInfo.isNotFound) {
return 404;
}
if (!hasPermission.value) {
return 403;
}
return null;
})
// 获取组织namespace
const { namespace } = getOrgInfo();
const route = useRoute();
// 获取仓库
const orgStore = orgInfoStore();
// 社区基本信息
const getCommunityInfoFun = async() => {
const params: types.commonGroupReqType = {
orgId: namespace.value
};
const { data, error } = await reqCatch(getCommunityInfo, params);
if (!error && data) {
orgStore.setCommunityUrl({
communityUrl: `https://${data.data.custom_domain}`,
communityactivityUrl: `https://${data.data.custom_domain}/activelist`
});
orgStore.setCommunityInfo(data.data);
}
};
const filterFn = ref<Function | null>(null);
const nameMap = new Map([
['WIKI', 'repoWiki'],
['ISSUE', 'repoIssues'],
['MERGE_REQUEST', 'repoMerge'],
['FORK', 'repoFork'],
['ANALYSIS', 'repoAnalysis'],
['DISCUSSION', 'repoDiscussion'],
['COMMUNITY', 'reqCommunity']
]);
async function getModuleState() {
const res = await getSettingsValues({
repoId: repoId.value,
group_id: namespace.value
});
if (res.data) {
const { modules } = res.data.data;
const vModules = new Set();
modules.forEach(({ key, value }) => {
// 判断是否获取社区基本信息,拿到社区跳转链接
if (key === 'COMMUNITY' && value === '1') {
getCommunityInfoFun();
}
if (nameMap.get(key) && value === '0') vModules.add(nameMap.get(key));
});
filterFn.value = (item: any) => {
const isAllowed = !item.permission || access_level.value >= setting.role[item.permission];
if (item.id === 'reqCommunity') {
const par = modules.find((item: any) => item.key === 'COMMUNITY');
return !!(par && par.value === '1');
}
const isEnabled = !vModules.has(item.id);
return isEnabled && isAllowed;
};
}
}
// @TODO 此处只用于repo切换时读取项目模块配置信息。是否直接从repo详情获取
watch(repoId, () => getModuleState(), { flush: 'post', immediate: true });
addEventListener('updateNaviBar', getModuleState);
onUnmounted(() => offEvent('updateNaviBar', getModuleState));
</script>
<style scoped lang="scss">
.breadcrumb {
:deep(.g-link) {
font-weight: 500;
color: var(--color-CG600);
line-height: 20px;
&:last-child {
color: var(--color-G900);
}
}
&-splitter {
font-weight: 500;
color: var(--color-CG600);
line-height: 20px;
padding: 0 2px;
}
}
.g-archived {
width: 100%;
padding: 8px;
background-color: var(--color-Y100);
font-weight: bold;
color: var(--color-G900);
line-height: 20px;
text-align: center;
border-bottom: 1px solid var(--color-Y200);
}
.g-repo-header {
background-color: #F9F9FB;
border-bottom: 1px solid #F1F1F8;
position: relative;
z-index: 1;
}
.g-repo-content {
.g-repo-content-width {
max-width: 1536px;
margin: 0 auto;
}
.g-repo-content-header {
border-bottom: 1px solid #f1f1f8;
background: #f9f9fb;
}
}
.g-repo-layout {
:deep(.devui-layout) {
background-color: #fff;
}
:deep(.devui-layout__content) {
background: #fff;
}
}
</style>