搜索结果列表页面开发
This commit is contained in:
64
src/components/Header/Create.vue
Normal file
64
src/components/Header/Create.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<d-dropdown class="g-header-create" :position="['bottom-end']" align="end">
|
||||
<d-button class="border-none w-84 h-32 btn-add">
|
||||
<GIcon name="gt-plane-add" size="16px" color="#9C9DB4" />
|
||||
<span>新建</span>
|
||||
</d-button>
|
||||
<template #menu>
|
||||
<div class="py-10 px-4">
|
||||
<OptionLink v-for="item in linkList" :key="item.label" v-bind="item" @click="handleClick(item, $event)">
|
||||
{{ item.label }}
|
||||
</OptionLink>
|
||||
</div>
|
||||
</template>
|
||||
</d-dropdown>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { orgInfoStore } from '@/stores/Org/index';
|
||||
import { Message } from 'vue-devui';
|
||||
|
||||
const orgStore = orgInfoStore();
|
||||
const linkList = ref([
|
||||
{ label: '新建项目', to: { name: 'newRepo', query: { position: 'nav_top' }}},
|
||||
{ label: '导入项目(URL)', to: { name: 'importRepo' }},
|
||||
{ label: '迁移项目', to: { name: 'migrateRepo' }, disabled: false },
|
||||
{ label: '新建组织', routeName: 'newOrg' }
|
||||
]);
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const handleClick = (linkItem, event) => {
|
||||
if (linkItem?.routeName === 'newOrg') {
|
||||
if (!orgStore.isCreateOrg) { // 超过用户可管理组织数量,禁止进入创建组织页面
|
||||
Message.error('你最多可以创建或管理 5 个组织,无法创建新的组织。');
|
||||
return;
|
||||
} else {
|
||||
router.push({name: 'newOrg'});
|
||||
}
|
||||
}
|
||||
// 针对创建项目,如果通过toolbar进入项目创建页面,强制刷新数据
|
||||
if (linkItem?.to?.name === 'newRepo' && linkItem?.to?.name === route.name) {
|
||||
window.location.reload();
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.btn-add {
|
||||
background-color: #000;
|
||||
color: #fff !important;
|
||||
width: 84px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
span {
|
||||
margin-left: 8px;
|
||||
}
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background-color: #333;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
176
src/components/Header/Entrance.vue
Normal file
176
src/components/Header/Entrance.vue
Normal file
@@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<d-dropdown
|
||||
v-if="!isMobile"
|
||||
class="g-entrance"
|
||||
:position="['bottom-start']"
|
||||
align="start"
|
||||
@toggle="handleToggle"
|
||||
>
|
||||
<div class="g-entrance-trigger cursor-pointer">
|
||||
<GIcon name="gt-list-view" size="20px" />
|
||||
</div>
|
||||
<template #menu>
|
||||
<EntranceMenu
|
||||
:linkList="linkList"
|
||||
:blockList="blockList"
|
||||
@handleToggle="handleToggle($event)"
|
||||
/>
|
||||
</template>
|
||||
</d-dropdown>
|
||||
<GIcon
|
||||
v-if="isMobile"
|
||||
class=""
|
||||
name="gt-hamburger"
|
||||
size="20px"
|
||||
@click="handleToggle"
|
||||
/>
|
||||
<d-drawer v-if="isMobile" v-model="visible" position="left">
|
||||
<EntranceMenu
|
||||
class="p-5"
|
||||
:linkList="linkList"
|
||||
:blockList="blockList"
|
||||
@handleToggle="handleToggle($event)"
|
||||
/>
|
||||
</d-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { getRepos } from '@/api/repo';
|
||||
import { getMyGroupsList } from '@/api/user';
|
||||
import { useAccountStore, entranceData } from '@/stores/user';
|
||||
import EntranceMenu from './EntranceMenu.vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
defineProps({
|
||||
isMobile: {
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const user = useAccountStore();
|
||||
const { username } = user.accountInfo;
|
||||
const { repoList, orgList } = storeToRefs(entranceData());
|
||||
const visible = ref(false);
|
||||
const handleToggle = (isVisible?: boolean) => {
|
||||
visible.value = !!isVisible;
|
||||
if (visible.value) init();
|
||||
};
|
||||
|
||||
// 头部模块连接
|
||||
const linkList = ref([
|
||||
{ title: '首页', icon: 'gt-home-c', href: '/' },
|
||||
{ title: '应用市场', icon: 'gt-package-c', to: { name: 'home' }, disabled: true }
|
||||
]);
|
||||
|
||||
/* 设置我的组织和项目列表 */
|
||||
const repoData = ref({
|
||||
id: 'repo',
|
||||
title: '最近活跃项目',
|
||||
list: [],
|
||||
avatarRound: true,
|
||||
loading: false,
|
||||
link: {
|
||||
to: {
|
||||
name: 'settingRepo',
|
||||
params: {
|
||||
namespace: username
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
const orgData = ref({
|
||||
id: 'org',
|
||||
title: '加入组织',
|
||||
list: [],
|
||||
avatarRound: false,
|
||||
loading: false,
|
||||
link: {
|
||||
to: {
|
||||
name: 'settingOrganization',
|
||||
params: {
|
||||
namespace: username
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
const blockList = computed(() => [repoData.value, orgData.value]);
|
||||
|
||||
const MAX_LIST = 5;
|
||||
const initRepoData = async() => {
|
||||
repoData.value.loading = true;
|
||||
|
||||
const repoParams = {
|
||||
order_by: 'last_activity_at',
|
||||
sort: 'desc',
|
||||
membership: true
|
||||
};
|
||||
if (!repoList.value?.length) {
|
||||
const res = await getRepos(repoParams);
|
||||
repoList.value = res?.data?.data?.content;
|
||||
}
|
||||
repoData.value.list = (repoList.value || []).slice(0, MAX_LIST).map((item:{name_with_namespace?:string, name?:string, namespace?:string}) => ({
|
||||
...item,
|
||||
label: item.name_with_namespace,
|
||||
avatarText: item.name,
|
||||
to: {
|
||||
path: `/${item.namespace}`
|
||||
}
|
||||
}));
|
||||
|
||||
repoData.value.loading = false;
|
||||
};
|
||||
const initOrgData = async() => {
|
||||
orgData.value.loading = true;
|
||||
|
||||
const orgParams = {
|
||||
page: 1,
|
||||
per_page: 10
|
||||
};
|
||||
if (!orgList.value?.length) {
|
||||
const res = await getMyGroupsList(orgParams);
|
||||
orgList.value = res?.data?.content;
|
||||
}
|
||||
orgData.value.list = (orgList.value || []).slice(0, MAX_LIST).map((item:{full_name:string, name:string, full_path:string}) => ({
|
||||
...item,
|
||||
label: item.full_name,
|
||||
avatarText: item.name,
|
||||
to: {
|
||||
name: 'homepage',
|
||||
params: {
|
||||
namespace: item.full_path
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
orgData.value.loading = false;
|
||||
};
|
||||
const init = () => {
|
||||
initRepoData();
|
||||
initOrgData();
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'devui-theme/styles-var/devui-var.scss';
|
||||
|
||||
.g-entrance-menu {
|
||||
padding: 20px;
|
||||
width: 260px;
|
||||
line-height: 32px;
|
||||
&-trigger {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
transition: background-color ease-out 0.5s;
|
||||
&:hover {
|
||||
background-color: #fbfbfb;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
118
src/components/Header/EntranceMenu.vue
Normal file
118
src/components/Header/EntranceMenu.vue
Normal file
@@ -0,0 +1,118 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
type LinkItem = {
|
||||
title: string;
|
||||
icon: string;
|
||||
to: { name: string };
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
type BlockItem = {
|
||||
id: string;
|
||||
title: string;
|
||||
list: any[];
|
||||
avatarRound: boolean;
|
||||
link: {
|
||||
to: {
|
||||
name: string;
|
||||
params: {
|
||||
namespace?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
defineProps<{
|
||||
linkList: LinkItem[];
|
||||
blockList: BlockItem[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{(event: 'handleToggle', value: boolean): void;
|
||||
}>();
|
||||
|
||||
const router = useRouter();
|
||||
// repo\org切换,强制页面重载
|
||||
const handleLinkClick = (event, data) => {
|
||||
const href = router.resolve(data.to).fullPath;
|
||||
window.open(href, '_self');
|
||||
emit('handleToggle', false);
|
||||
|
||||
event?.stopPropagation();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="g-entrance-menu">
|
||||
<div class="g-entrance-menu-links">
|
||||
<OptionLink v-for="item in linkList" :key="item.title" v-bind="item">
|
||||
<GIcon :name="item.icon" />{{ item.title }}
|
||||
</OptionLink>
|
||||
</div>
|
||||
<div class="g-entrance-menu-block" v-for="block in blockList" :key="block.id">
|
||||
<div class="g-entrance-menu-block-title">
|
||||
<span>{{ block.title }}</span>
|
||||
<GLink v-bind="block.link">
|
||||
<GIcon name="gt-more-operate" />
|
||||
</GLink>
|
||||
</div>
|
||||
<div class="g-entrance-menu-block-list">
|
||||
<DataPanel :loading="block.loading" :empty="!block.list?.length" :card="false" skeleton>
|
||||
<OptionLink
|
||||
v-for="item in block.list"
|
||||
:key="item.name"
|
||||
v-bind="item"
|
||||
@click="handleLinkClick($event, item)"
|
||||
>
|
||||
<GAvatar
|
||||
:src="item.avatar"
|
||||
:name="item.avatarText"
|
||||
:width="16"
|
||||
:height="16"
|
||||
:is_round="block.avatarRound"
|
||||
></GAvatar>
|
||||
<d-tooltip :content="item.label" position="right" :mouse-enter-delay="800">
|
||||
<div class="ellipsis">{{ item.label }}</div>
|
||||
</d-tooltip>
|
||||
</OptionLink>
|
||||
<template #empty>
|
||||
<div class="py-2 flex-center opacity-50">暂无数据</div>
|
||||
</template>
|
||||
<template #loading>
|
||||
<d-skeleton :rows="5" />
|
||||
</template>
|
||||
</DataPanel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'devui-theme/styles-var/devui-var.scss';
|
||||
|
||||
.g-entrance-menu {
|
||||
&-links {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
&-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-top: 1px solid var(--color-border-light);
|
||||
margin-top: 16px;
|
||||
&:last-child {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
&-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #2d2d2e;
|
||||
line-height: 32px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
58
src/components/Header/Notice.vue
Normal file
58
src/components/Header/Notice.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<router-link to="/notice?status=all&message_type=mr">
|
||||
<d-badge class="g-notice" :count="counts" :offset="[-2, 4]" :hidden="!counts" status="danger">
|
||||
<GIcon name="gt-line-notice" />
|
||||
</d-badge>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue';
|
||||
import { getMessageCount } from '@/api/notice';
|
||||
import { addEventListener, offEvent } from '@/utils/eventBus';
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
const counts = ref(0);
|
||||
const getCounts = () => {
|
||||
getMessageCount().then((result) => {
|
||||
counts.value = result.data?.reduce((prev: number, curr: Record<string, any>) => {
|
||||
return prev + curr.un_read_count;
|
||||
}, 0);
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getCounts();
|
||||
addEventListener('updateNotice', debounce((count: number) => {
|
||||
counts.value = count;
|
||||
}, 500));
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
offEvent('updateNotice');
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'devui-theme/styles-var/devui-var.scss';
|
||||
.g-notice {
|
||||
cursor: pointer;
|
||||
margin: 0 8px;
|
||||
&-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 8px;
|
||||
&-item {
|
||||
width: 280px;
|
||||
padding: 8px 0;
|
||||
&-title {
|
||||
font-weight: 500;
|
||||
}
|
||||
&-content {
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
25
src/components/Header/README.md
Normal file
25
src/components/Header/README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Header
|
||||
|
||||
### 说明
|
||||
- 全局通用Header组件
|
||||
|
||||
``` js
|
||||
<Header />
|
||||
|
||||
<Header>
|
||||
<template #info>
|
||||
这是左侧插槽
|
||||
</template>
|
||||
<template #menu>
|
||||
这是右侧插槽,一般用于插入菜单Tab组件
|
||||
</template>
|
||||
</Header>
|
||||
```
|
||||
|
||||
|
||||
### Slots
|
||||
|
||||
| 名称 | 说明 |
|
||||
|---------------------|---------------------------------------------|
|
||||
| info | 第二行左侧,常用语显示用户信息等 |
|
||||
| menu | 第二行右侧,常用于定义路由Tab等 |
|
||||
84
src/components/Header/Search.vue
Normal file
84
src/components/Header/Search.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<d-input
|
||||
:key="$route.path + $route.query?.val" id="golbalSearch" class="g-header-search"
|
||||
v-model.trim="searchStr" placeholder="输入搜索内容..." @keyup.enter="search" @clear="handleClear" clearable >
|
||||
<template #prepend>
|
||||
<d-select v-model="searchType" :options="options"></d-select>
|
||||
</template>
|
||||
<template #append>
|
||||
<d-icon name="search" style="font-size: inherit;" />
|
||||
</template>
|
||||
</d-input>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch, onUnmounted, reactive } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import microApp from '@micro-zoe/micro-app';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const searchStr = ref(route?.query?.val || '');
|
||||
const searchType = ref('License');
|
||||
const options = reactive(['License']);
|
||||
|
||||
const placeholder = ref('「开搜」:一搜即达,开发者的AI搜索');
|
||||
|
||||
const search = () => {
|
||||
if (!searchStr.value) {
|
||||
document.querySelector('#golbalSearch').blur();
|
||||
return;
|
||||
}
|
||||
router.push({ name: 'search', query: { val: searchStr.value || '', type: route?.query?.type || 'License' }});
|
||||
};
|
||||
const handleClear = () => {
|
||||
router.push({ name: 'home' });
|
||||
};
|
||||
|
||||
watch(() => route.query?.val, (newVal) => {
|
||||
microApp.setData('micoro-app-homeweb-app', { type: 'search', data: newVal || '' });
|
||||
});
|
||||
|
||||
const handleDataChange = ({ name, type, data }) => {
|
||||
if (name === 'micoro-app-homeweb-app') {
|
||||
if (type === 'keyword-change') {
|
||||
searchStr.value = data;
|
||||
}
|
||||
if (type === 'route-change') {
|
||||
//
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
});
|
||||
onUnmounted(() => {
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'devui-theme/styles-var/devui-var.scss';
|
||||
|
||||
.g-header-search {
|
||||
border: 1px solid #5E7CE0;
|
||||
background: #FFF;
|
||||
//box-shadow: 0px 2px 5px 1px rgba(0, 0, 0, 0.05) inset;
|
||||
&-icon {
|
||||
opacity: .5;
|
||||
transition: opacity .5s ease;
|
||||
}
|
||||
.devui-input--focus {
|
||||
.g-header-search-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
::v-deep .devui-input-slot__prepend{
|
||||
width: 140px;
|
||||
}
|
||||
::v-deep .devui-input-slot__append{
|
||||
width: 40px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
86
src/components/Header/SearchOld.vue
Normal file
86
src/components/Header/SearchOld.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<d-input
|
||||
:key="$route.path + $route.query?.val"
|
||||
id="golbalSearch"
|
||||
class="g-header-search"
|
||||
v-model.trim="searchStr"
|
||||
:placeholder="placeholder"
|
||||
clearable
|
||||
@keyup.enter="search"
|
||||
@focus="placeholder = '「开搜」:输入搜索内容...'"
|
||||
@blur="placeholder = '「开搜」:一搜即达,开发者的AI搜索'"
|
||||
autocomplete="off"
|
||||
>
|
||||
<template #prefix>
|
||||
<GIcon name="gt-search" class="g-header-search-icon" />
|
||||
</template>
|
||||
</d-input>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch, onUnmounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import microApp from '@micro-zoe/micro-app';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const searchStr = ref(route?.query?.val || '');
|
||||
|
||||
const placeholder = ref('「开搜」:一搜即达,开发者的AI搜索');
|
||||
|
||||
const search = () => {
|
||||
if (!searchStr.value) {
|
||||
document.querySelector('#golbalSearch').blur();
|
||||
return;
|
||||
}
|
||||
|
||||
if (microApp.router.current.has('micoro-app-homeweb-app')) {
|
||||
microApp.router.push({
|
||||
name: 'micoro-app-homeweb-app',
|
||||
path: router.resolve({ name: 'search', query: { val: searchStr.value, type: route?.query?.type || 'repo' }}).href
|
||||
});
|
||||
} else {
|
||||
router.push({ name: 'search', query: { val: searchStr.value || '', type: route?.query?.type || 'repo' }});
|
||||
}
|
||||
};
|
||||
|
||||
watch(() => route.query?.val, (newVal) => {
|
||||
microApp.setData('micoro-app-homeweb-app', { type: 'search', data: newVal || '' });
|
||||
});
|
||||
|
||||
const handleDataChange = ({ name, type, data }) => {
|
||||
if (name === 'micoro-app-homeweb-app') {
|
||||
if (type === 'keyword-change') {
|
||||
searchStr.value = data;
|
||||
}
|
||||
if (type === 'route-change') {
|
||||
//
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
microApp.addDataListener('micoro-app-homeweb-app', handleDataChange, false);
|
||||
});
|
||||
onUnmounted(() => {
|
||||
microApp.removeDataListener('micoro-app-homeweb-app', handleDataChange);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import 'devui-theme/styles-var/devui-var.scss';
|
||||
|
||||
.g-header-search {
|
||||
background: #FFF;
|
||||
box-shadow: 0px 2px 5px 1px rgba(0, 0, 0, 0.05) inset;
|
||||
&-icon {
|
||||
opacity: .5;
|
||||
transition: opacity .5s ease;
|
||||
}
|
||||
.devui-input--focus {
|
||||
.g-header-search-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
195
src/components/Header/UserAvatar.vue
Normal file
195
src/components/Header/UserAvatar.vue
Normal file
@@ -0,0 +1,195 @@
|
||||
<template>
|
||||
<div class="g-user-avatar flex-center ml-1" @click="toggleDropDown">
|
||||
<GAvatar :name="accountInfo.nickname" :src="accountInfo.avatar"></GAvatar>
|
||||
</div>
|
||||
<d-drawer v-model="visible" class="g-user-drawer-container">
|
||||
<div class="g-user-drawer">
|
||||
<div class="g-user-drawer-info">
|
||||
<div class="g-user-avatar">
|
||||
<GAvatar :name="accountInfo.nickname" :src="accountInfo.avatar"></GAvatar>
|
||||
</div>
|
||||
<div class="g-user-drawer-info-list">
|
||||
<div class="g-user-drawer-info-list-name">
|
||||
<span class="break-all">{{ accountInfo.nickname }}</span>
|
||||
<GIcon class="close-icon" name="gt-close" size="12" @click="handleClose" />
|
||||
</div>
|
||||
<div class="g-user-drawer-info-list-id break-all">@{{ accountInfo.username }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="g-user-drawer-block" v-for="(block, index) in blockList" :key="index">
|
||||
<OptionLink v-for="item in block" :key="item.id" v-bind="item" @click="handleClick(item)">
|
||||
<GIcon v-if="item.icon" :name="item.icon" size="14px" />{{ item.label }}
|
||||
</OptionLink>
|
||||
</div>
|
||||
</div>
|
||||
</d-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useAccount } from '@/utils/hooks/useAccount';
|
||||
import { toLogout } from '@/api/user';
|
||||
import { devLogout } from '@/api/org/devIndex';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { reqCatch } from '@/utils/catch';
|
||||
import { orgInfoStore } from '@/stores/Org/index';
|
||||
import { repoInfoStore } from '@/stores/Repo';
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
const { accountInfo, RemoveInfo } = useAccount();
|
||||
const { repoInfo } = storeToRefs(repoInfoStore()); // pinia直接解构会失去响应式
|
||||
const { orgInfo } = storeToRefs(orgInfoStore()); // pinia直接解构会失去响应式
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const visible = ref(false);
|
||||
const toggleDropDown = (isShow) => {
|
||||
if (typeof isShow === 'boolean') {
|
||||
visible.value = isShow;
|
||||
} else {
|
||||
visible.value = !visible.value;
|
||||
}
|
||||
};
|
||||
|
||||
const logout = async() => {
|
||||
const res = await toLogout();
|
||||
await reqCatch(devLogout, {}); // 清除社区登录
|
||||
if (!res.error) {
|
||||
RemoveInfo();
|
||||
// 有权限刷新当前页,无权限需要登录返回首页
|
||||
if (route.meta?.needLogin || (route.meta?.type === 'repo' && repoInfo.value?.visibility === 'private') || orgInfo.value?.visibility === 'private') {
|
||||
router.push('/');
|
||||
} else {
|
||||
router.go(0);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleClick = (item) => {
|
||||
if (item?.action) {
|
||||
item.action();
|
||||
}
|
||||
|
||||
toggleDropDown(false);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
toggleDropDown(false);
|
||||
};
|
||||
|
||||
// 使用router-link跳转,默认参数结构如下:
|
||||
const DEFAULT_ROUTER_PARAMS = Object.freeze({
|
||||
name: '',
|
||||
params: {
|
||||
namespace: accountInfo.value.username
|
||||
},
|
||||
query: {
|
||||
}
|
||||
});
|
||||
// 获取router跳转参数
|
||||
const getRouterLink = params => {
|
||||
if (typeof params === 'string') {
|
||||
return {
|
||||
...DEFAULT_ROUTER_PARAMS,
|
||||
name: params
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...DEFAULT_ROUTER_PARAMS,
|
||||
...params
|
||||
};
|
||||
};
|
||||
|
||||
const blockList = ref([
|
||||
[
|
||||
{ id: 'myPage', label: '我的主页', to: getRouterLink('homepage') }
|
||||
],
|
||||
[
|
||||
{ id: 'dashboard', label: '工作台', icon: 'gt-member-c', to: getRouterLink('dashboard') },
|
||||
{ id: 'myIssues', label: '我的 Issue', icon: 'gt-issue-c', to: getRouterLink('issues') },
|
||||
{ id: 'myDiscuss', label: '我的讨论', icon: 'gt-comment-c', to: getRouterLink('discussionCreated') },
|
||||
{ id: 'myMR', label: '我的合并请求', icon: 'gt-me-merge-c', to: getRouterLink('merge') },
|
||||
{ id: 'notice', label: '消息通知', icon: 'gt-remind-c', to: getRouterLink({ name: 'notice', query: { status: 'all' }}) }
|
||||
],
|
||||
[
|
||||
{ id: 'myOrg', label: '我的组织', icon: 'gt-organizations-c', to: getRouterLink({ name: 'settingOrganization' }) },
|
||||
{ id: 'myRepo', label: '我的项目', icon: 'gt-folder-c', to: getRouterLink('userRepos') },
|
||||
{ id: 'myStar', label: '我的 Star', icon: 'gt-star-c', to: getRouterLink('userStars') }
|
||||
],
|
||||
[
|
||||
{ id: 'personalSetting', label: '个人设置', icon: 'gt-setting-c', to: getRouterLink('setting') },
|
||||
{ id: 'help', label: '帮助文档', icon: 'gt-file-c', href: 'https://gitcode.com/Gitcode-offical-team/GitCode-Docs/wiki' }
|
||||
],
|
||||
[
|
||||
{ id: 'logout', label: '退出登录', icon: 'gt-exit', action: logout }
|
||||
]
|
||||
]);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'devui-theme/styles-var/devui-var.scss';
|
||||
.g-user {
|
||||
&-avatar {
|
||||
height: 35px;
|
||||
width: 35px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
margin-left: 2px;
|
||||
}
|
||||
&-drawer {
|
||||
padding: 24px;
|
||||
&-info {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding-bottom: 16px;
|
||||
gap: 16px;
|
||||
.g-user-avatar {
|
||||
cursor: inherit;
|
||||
}
|
||||
&-list {
|
||||
flex: 1;
|
||||
&-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #2D2D2E;
|
||||
line-height: 20px;
|
||||
.close-icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
&-id {
|
||||
font-size: 12px;
|
||||
color: #707A87;
|
||||
line-height: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px 0;
|
||||
& + & {
|
||||
border-top: 1px solid var(--color-border-light);
|
||||
}
|
||||
:deep(.g-option-link) {
|
||||
line-height: 36px;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.g-user-drawer-container {
|
||||
border-bottom-right-radius: 0 !important;
|
||||
border-top-right-radius: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
</style>
|
||||
257
src/components/Header/index.vue
Normal file
257
src/components/Header/index.vue
Normal file
@@ -0,0 +1,257 @@
|
||||
<template>
|
||||
<div class="full-w" :style="{ height: `${height}px` }">
|
||||
<div class="g-header fixed z-20" ref="headerRef">
|
||||
<div class="g-toolbar w-full overflow-hidden" :class="[searchClass, route?.meta?.headerClassName]">
|
||||
<div class="g-toolbar-left flex-shrink items-center">
|
||||
<div class="flex-center gap-[16px]">
|
||||
<!-- <g-icon v-if="!asideSetShow" @click="$emit('clickMenu')" name="gt-hamburger" size="20px" class="cursor-pointer hover:!text-black" color="#1D1F3A"></g-icon>-->
|
||||
<GLink class="g-toolbar-left-logo" href="/" />
|
||||
<GLink class="g-toolbar-left-title" href="/">可信代码库</GLink>
|
||||
|
||||
<!-- <Entrance v-if="isLogin" :is-mobile="isMobile" /> -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 flex items-center justify-end">
|
||||
<Search class="g-toolbar-left-search" v-if="!isMobile" :key="$route.fullPath"/>
|
||||
</div>
|
||||
<div class="g-toolbar-right gap-[24px] flex-grow-0 flex-shrink-0">
|
||||
<GLink
|
||||
class="g-toolbar-right-link"
|
||||
v-for="item in defaultLinkList"
|
||||
:key="item.label"
|
||||
:to="item.to"
|
||||
:href="item.href"
|
||||
:target="item.target"
|
||||
>
|
||||
{{ item.label }}
|
||||
</GLink>
|
||||
<template v-if="!isLogin">
|
||||
<d-button class="border-none w-84 h-32 btn-add hidden-m" @click="handleLogin('login')" v-if="inSearch">
|
||||
<GIcon name="gt-plane-add" size="16px" color="#9C9DB4" />
|
||||
<span>新建</span>
|
||||
</d-button>
|
||||
<div class="cursor-pointer hidden-m" @click="handleLogin('login')" v-if="inSearch"><span>搜索记录</span></div>
|
||||
<d-button variant="solid" color="primary" @click="handleLogin('login')">登录</d-button>
|
||||
<d-button @click="handleLogin('register')" class="min-w-[64px]">注册</d-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<!--ai copilot-->
|
||||
<a class="g-header-copilot" href="/ai-copilot/">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.5 6C2.5 4.89543 3.39543 4 4.5 4H11.5C12.6046 4 13.5 4.89543 13.5 6V12C13.5 13.1046 12.6046 14 11.5 14H4.5C3.39543 14 2.5 13.1046 2.5 12V6ZM4.5 8C4.5 7.44772 4.94772 7 5.5 7C6.05228 7 6.5 7.44772 6.5 8V9C6.5 9.55228 6.05228 10 5.5 10C4.94772 10 4.5 9.55228 4.5 9V8ZM10.5 7C9.94772 7 9.5 7.44772 9.5 8V9C9.5 9.55228 9.94772 10 10.5 10C11.0523 10 11.5 9.55228 11.5 9V8C11.5 7.44772 11.0523 7 10.5 7Z" fill="white"/>
|
||||
<path opacity="0.6" fill-rule="evenodd" clip-rule="evenodd" d="M8.75 3.29933C9.19835 3.03997 9.5 2.55521 9.5 2C9.5 1.17157 8.82843 0.5 8 0.5C7.17157 0.5 6.5 1.17157 6.5 2C6.5 2.55521 6.80165 3.03997 7.25 3.29933V5.5H8.75V3.29933ZM0 7C0 6.44772 0.447715 6 1 6H1.5V12H1C0.447715 12 0 11.5523 0 11V7ZM15 6H14.5V12H15C15.5523 12 16 11.5523 16 11V7C16 6.44772 15.5523 6 15 6Z" fill="white"/>
|
||||
</svg>
|
||||
<span class="ml-xs">AI Copilot</span>
|
||||
</a>
|
||||
<Create />
|
||||
<SearchAiHistory />
|
||||
<Notice />
|
||||
<UserAvatar />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="$slots.info || $slots.menu || $slots.bodyHeader" class="g-header-body">
|
||||
<slot name="bodyHeader">
|
||||
<div class="g-header-body-left h-full">
|
||||
<slot name="info" />
|
||||
</div>
|
||||
<div class="g-header-body-right h-full">
|
||||
<slot name="menu"></slot>
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
<div v-if="$slots.headerBottom">
|
||||
<slot name="headerBottom"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onBeforeUnmount, ref, watchEffect, computed } from 'vue';
|
||||
import { useElementSize } from '@vueuse/core';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useAccountStore } from '@/stores/user';
|
||||
import { emitEvent } from '@/utils/eventBus';
|
||||
import Create from './Create.vue';
|
||||
import Entrance from './Entrance.vue';
|
||||
import Notice from './Notice.vue';
|
||||
import Search from './Search.vue';
|
||||
import SearchAiHistory from './searchAiHistory.vue';
|
||||
import UserAvatar from './UserAvatar.vue';
|
||||
import { useWindowScroll } from '@vueuse/core';
|
||||
import { usePageResize } from '@/utils/hooks/usePageResize';
|
||||
import microApp from '@micro-zoe/micro-app';
|
||||
|
||||
const { y } = useWindowScroll();
|
||||
const defaultLinkList = ref([]);
|
||||
const route = useRoute();
|
||||
const inSearch = ref(false);
|
||||
const searchClass = computed(() => ({ 'border-b-transparent': inSearch.value && y.value === 0 }));
|
||||
// header fixed布局高度自适应
|
||||
const headerRef = ref(null);
|
||||
const { height = 56 } = useElementSize(headerRef);
|
||||
|
||||
const userInfo = useAccountStore();
|
||||
const { isLogin } = storeToRefs(userInfo);
|
||||
const showSearch = computed(() => route.name !== 'home' && route.name !== 'search');
|
||||
const handleLogin = (type) => {
|
||||
emitEvent('login', { type, triggerType: '主动注册' });
|
||||
};
|
||||
const { isMobile } = usePageResize();
|
||||
|
||||
/* store 用户信息 */
|
||||
/* @test 后面将清除 */
|
||||
watchEffect(() => {
|
||||
|
||||
});
|
||||
microApp.router.beforeEach({
|
||||
'micoro-app-homeweb-app': (to, from) => {
|
||||
if (to.pathname === '/search') {
|
||||
inSearch.value = true;
|
||||
} else {
|
||||
inSearch.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
defineExpose({
|
||||
height
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'devui-theme/styles-var/devui-var.scss';
|
||||
|
||||
$radius: 4px;
|
||||
$add-radius: 48px;
|
||||
$header-bg: #fff;
|
||||
|
||||
.g-header {
|
||||
// overflow: hidden;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
justify-content: space-between;
|
||||
background-color: $header-bg;
|
||||
:deep(.g-link) {
|
||||
color: $devui-text-weak;
|
||||
}
|
||||
.g-header-copilot {
|
||||
background: rgba(41, 81, 224, 1);
|
||||
color: #FFF;
|
||||
width: 120px;
|
||||
border-radius: 48px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
justify-content: center;
|
||||
&:hover {
|
||||
background: rgba(41, 81, 224, .8);
|
||||
}
|
||||
}
|
||||
.g-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex: 1;
|
||||
gap: 16px;
|
||||
flex-basis: 56px;
|
||||
overflow: hidden;
|
||||
padding: 0 32px;
|
||||
border-bottom: 1px solid #F1F1F8;
|
||||
:deep(.devui-button) {
|
||||
padding: 6px 16px;
|
||||
border-radius: $add-radius;
|
||||
}
|
||||
&-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 25px;
|
||||
&-logo {
|
||||
display: inline-block;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background: url(../../assets/imgs/logo/logo.png) no-repeat;
|
||||
background-size: contain;
|
||||
vertical-align: middle;
|
||||
background-position: 0 center;
|
||||
}
|
||||
&-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
&-search {
|
||||
max-width: 340px;
|
||||
border-radius: $radius;
|
||||
}
|
||||
}
|
||||
&-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@media screen and (max-width: 576px) {
|
||||
@apply gap-[16px];
|
||||
}
|
||||
}
|
||||
&.border-b-transparent {
|
||||
border-bottom-color: transparent !important;
|
||||
}
|
||||
&.border-b-show {
|
||||
border-bottom-color: #F1F1F8 !important;
|
||||
}
|
||||
}
|
||||
.g-header-gray {
|
||||
@apply bg-[#F9F9FB];
|
||||
}
|
||||
&-body {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 32px;
|
||||
background-color: $header-bg;
|
||||
border-bottom: 1px solid $devui-dividing-line;
|
||||
flex-basis: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1000px) {
|
||||
.g-header {
|
||||
.g-toolbar {
|
||||
padding: 20px;
|
||||
border-color: transparent;
|
||||
}
|
||||
.g-toolbar-left-logo {
|
||||
width: 28px;
|
||||
background-size: cover;
|
||||
}
|
||||
&-body {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
.btn-add {
|
||||
background-color: #000;
|
||||
color: #fff !important;
|
||||
width: 84px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
span {
|
||||
margin-left: 8px;
|
||||
}
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background-color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width:576px) {
|
||||
.hidden-m {
|
||||
display: none;
|
||||
}
|
||||
.g-header-copilot {
|
||||
display: none!important;;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
18
src/components/Header/searchAiHistory.vue
Normal file
18
src/components/Header/searchAiHistory.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="cursor-pointer" @click="triggerRec"><span>搜索记录</span></div>
|
||||
<HistoryRecord ref="searchRec"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import HistoryRecord from '@/components/historyRecord/index.vue';
|
||||
const searchRec = ref(null);
|
||||
const triggerRec = () => {
|
||||
searchRec.value.open();
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user