搜索结果列表页面开发

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,37 @@
<template>
<div class="choice-item g-card">
<div class="choice-item-header">
<gc-skeleton-item style="width: 100px; height: 24px;" />
<gc-skeleton-item style="width: 30px; height: 24px;" />
</div>
<div class="choice-item-content">
<gc-skeleton-item
style="
width: 80px;
height: 22px;
border-radius: 22px;
margin-top: 12px;
"
/>
<gc-skeleton-item style="height: 14px; margin-top: 0.75rem;" />
<gc-skeleton-item style="width: 50%; height: 14px; margin-top: 0.75rem;" />
</div>
<div class="choice-item-footer">
<gc-skeleton-item style="width: 50px; height: 14px;" />
<gc-skeleton-item style="width: 50px; height: 14px;" />
<gc-skeleton-item style="width: 50px; height: 14px;" />
</div>
</div>
</template>
<style scoped lang="scss">
.choice-item {
@apply min-h-[200px] p-[20px];
.choice-item-header {
@apply flex items-center justify-between pb-[20px];
}
.choice-item-footer {
@apply flex items-center gap-4 mt-3;
}
}
</style>

View File

@@ -0,0 +1,284 @@
<script setup lang="ts">
defineOptions({ name: 'GlobalChoice' });
import { ref, reactive, onMounted } from 'vue';
import { reqCatch } from '@/utils/catch';
import { globalFeaturedGroups, globalFeaturedProjects } from '@/api/home';
import type { projectSearchInfo } from '@/api/home/types';
import PSkeleton from './SkeletonProject.vue';
import LoadingMore from '@/components/LoadingMore/index.vue';
const loading = ref(false);
const groupList = ref<{ id: string; name: string }[]>([]);
const projectList = ref<projectSearchInfo[]>([]);
const activeId = ref('');
const pager = reactive({
page: 1,
pageSize: 12,
total: 0,
loading: false,
error: false,
end: false
});
// 获取精选组织
const fetchFeaturedGroups = async() => {
loading.value = true;
const result = await reqCatch(globalFeaturedGroups);
loading.value = false;
if (!result.error) {
const data = result.data.data;
groupList.value = data;
}
};
fetchFeaturedGroups();
const handleClickTag = (key: string = '') => {
if (activeId.value === key) return;
activeId.value = key;
pager.page = 1;
pager.end = false;
fetchProjects();
};
// 获取精选项目
const fetchProjects = async() => {
if (pager.error) pager.error = false;
pager.loading = true;
const result = await reqCatch(globalFeaturedProjects, {
group_id: activeId.value,
page: pager.page,
per_page: pager.pageSize
});
pager.loading = false;
if (!result.error) {
const { content = [], total = 0, page_num = 0 } = result.data.data;
pager.total = total;
const list = content.map((item: projectSearchInfo) => {
item.main_repository_language = item?.main_repository_language?.filter((e) => !!e) || [];
return item;
});
if (pager.page === 1) {
projectList.value = list;
} else {
projectList.value = projectList.value.concat(list);
}
pager.end = content.length < pager.pageSize;
} else {
pager.error = true;
}
};
const loadNext = () => {
pager.page++;
fetchProjects();
};
onMounted(() => {
fetchProjects();
});
</script>
<template>
<div>
<ul class="global-choice g-hidden-scrollbar">
<d-skeleton :loading="loading">
<li :class="['choice', { active: activeId === '' }]" @click="handleClickTag('')">
<a>全球精选</a>
</li>
<li
:class="['choice', { active: activeId === item.id }]"
v-for="item in groupList"
:key="item.id"
@click="handleClickTag(item.id)"
>
<a>{{ item.name }}</a>
</li>
<template #placeholder>
<gc-skeleton-item
v-for="i in 3"
:key="i"
style="width: 100px; height: 36px; border-radius: 36px; display: inline-block"
/>
</template>
</d-skeleton>
</ul>
<div class="global-choice-list">
<d-skeleton :loading="pager.loading && !projectList[0]">
<div class="global-choice-item" v-for="item in projectList" :key="item.id">
<!-- :to="`/${item.path_with_namespace || item.namespace}`" -->
<div class="choice-item g-card">
<div class="choice-item-header">
<h5>
<GLink :href="item.web_url">{{ item.name }}</GLink>
</h5>
<d-popover
content="近7天新增Star"
:position="['top']"
trigger="hover"
:mouse-enter-delay="500"
style="background-color: #fff; color: #2d2d2e"
>
<div class="choice-item-stat">
<GIcon name="gt-star7day" class="stat-icon" color="var(--devui-primary, #3b82f6)"></GIcon>
<GNumber :number="item.star_count_seven || 0" />
</div>
</d-popover>
</div>
<div class="choice-item-content">
<!-- <div class="tags" v-if="item.topic">
<GLink class="tag" :href="`/topic/${t.name}`" v-for="t in item.topic" :key="t.topic_id">{{
t.name
}}</GLink>
</div> -->
<p class="desc">
<GLink :href="item.web_url">{{ item.description }}</GLink>
</p>
</div>
<div class="choice-item-footer">
<div class="extented-item lang" v-if="item.main_repository_language[0]">
<span class="lang-color" :style="{ backgroundColor: item.main_repository_language[1] }"></span>
<span>{{ item.main_repository_language[0] || '' }}</span>
</div>
<div class="extented-item">
<GIcon name="gt-star"></GIcon>
<GNumber :number="item.star_count || 0" />
</div>
<div class="extented-item" v-if="item.license">
<GIcon name="gt-license"></GIcon>
<span>{{ item.license }}</span>
</div>
</div>
</div>
</div>
<template #placeholder>
<div class="global-choice-item" v-for="i in 3" :key="i">
<PSkeleton />
</div>
</template>
</d-skeleton>
</div>
<LoadingMore
v-if="projectList[0]"
:loading="pager.loading"
:error="pager.error"
:end="pager.end"
@click="loadNext"
/>
</div>
</template>
<style lang="scss" scoped>
.global-choice {
line-height: 56px;
@apply text-center;
.choice {
background-color: var(--color-CG700);
@apply cursor-pointer inline-block rounded-[20px] leading-[24px] py-2 px-4 mr-2 text-white font-bold leading-[20px];
&:last-of-type {
@apply mr-0;
}
&.active {
background-color: var(--devui-primary, #3b82f6);
}
&:not(.active):hover {
background-color: var(--color-G900, #2d2d2e);
}
a {
color: inherit;
}
}
@media screen and (max-width: 576px) {
white-space: nowrap;
// @apply overflow-hidden;
overflow-y: scroll;
margin-left: -12px;
margin-right: -12px;
.choice:first-of-type {
margin-left: 12px;
}
.choice:last-of-type {
margin-right: 12px;
}
}
}
.global-choice-list {
margin: 0 -10px;
@apply flex flex-wrap mt-[40px];
}
.global-choice-item {
flex-basis: 33.333%;
padding: 10px;
@apply overflow-hidden;
@media screen and (max-width: 768px) {
flex-basis: 50%;
}
@media screen and (max-width: 576px) {
flex-basis: 100%;
padding: 8px;
}
}
.choice-item {
@apply h-[200px] p-[20px] flex flex-col overflow-hidden;
&:hover {
box-shadow: 0px 2px 12px 0px rgba(37, 45, 59, 0.15);
}
.choice-item-header {
@apply flex items-center justify-between pb-[20px] gap-2;
h5 {
font-size: 20px;
font-family: NotoSansSC, NotoSansSC;
line-height: 30px;
@apply font-bold overflow-hidden text-ellipsis whitespace-nowrap;
}
.choice-item-stat {
font-size: 20px;
font-family: NotoSansSC, NotoSansSC;
font-weight: 600;
color: var(--devui-primary, #3b82f6);
@apply flex items-center gap-2 whitespace-nowrap;
}
}
.choice-item-content {
@apply flex-1;
.tags {
@apply overflow-hidden text-ellipsis whitespace-nowrap my-2;
.tag {
max-width: 108px;
@apply overflow-hidden text-ellipsis;
background-color: var(--color-CG500);
@apply text-xs text-white inline-block px-[10px] py-[3px] leading-[16px] rounded-[22px] mr-2;
&:last-of-type {
@apply mr-0;
}
}
}
.desc {
font-family: NotoSansSC, NotoSansSC;
@apply text-G900 line-clamp-2 leading-[20px] text-sm break-words;
}
}
.choice-item-footer {
@apply text-xs leading-[16px] mt-3 text-CG600 whitespace-nowrap overflow-hidden text-ellipsis break-words;
.extented-item {
@apply inline-flex items-center gap-2 mr-[17px] text-ellipsis overflow-hidden;
&:last-of-type {
@apply mr-0;
}
&.lang {
vertical-align: top;
}
}
.lang-color {
background-color: var(--color-danger);
@apply inline-block w-[4px] h-[12px] rounded-[1px];
}
}
}
</style>

View File

@@ -0,0 +1,395 @@
<script lang="ts" setup>
defineOptions({ name: 'SearchTool' });
import { homeSearchProject } from '@/api/home';
import type { projectSearchInfo } from '@/api/home/types';
import { ref } from 'vue';
import { useElementSize, useLocalStorage } from '@vueuse/core';
import debounce from 'lodash/debounce';
import { reqCatch } from '@/utils/catch';
import { useRouter } from 'vue-router';
const searchValue = ref('');
const visible = ref(false);
const inputIng = ref(false);
const loading = ref(false);
const refInputGroup = ref(null);
const searchHistoryList = useLocalStorage<{ value: string; deletable?: boolean }[]>('search-history', []);
const searchList = ref<projectSearchInfo[]>([]);
const router = useRouter();
const { width } = useElementSize(refInputGroup);
const onKeyupEnter = (e: KeyboardEvent) => {
goSearch();
};
const handleToggle = (blo: boolean) => {
visible.value = blo;
};
const onInput = debounce((str: string) => {
if (inputIng.value) return;
handleSearch(str);
}, 500);
const handleClear = () => {
searchValue.value = '';
searchList.value = [];
};
/**
* 延迟获取输入值
* 避免从中文输入时跳转
*/
const goSearch = (str: string | MouseEvent = '') => {
setTimeout(() => {
// 点击事件重置
if (typeof str === 'object') str = '';
str = str || searchValue.value;
str = str.trim();
router.push(`/search?val=${str}`);
saveSearchKey(str);
}, 0);
};
/**
* 保存搜索关键词,去重,去空值
*/
const saveSearchKey = (str: string = '') => {
if (str && !searchHistoryList.value.some((e) => e.value === str)) {
const list = searchHistoryList.value.slice(0);
list.unshift({ value: str });
list.splice(10);
searchHistoryList.value = list;
}
};
/**
* 搜索关键词,展示命中的项目
*/
const handleSearch = async(str: string = '') => {
str = str.trim();
if (!str) {
searchList.value = [];
return;
}
const result = await reqCatch(homeSearchProject, { search: str || '' });
if (!result.error) {
const data = result.data.data;
searchList.value = data;
}
};
const handleToProject = (item: projectSearchInfo) => {
window.open(`/${item.path_with_namespace}`);
};
/**
* 禁止已聚焦时弹框变化
*/
const handleInputClick = (e: Event) => {
const inputNode = document.querySelector('.search-group .search-tool input');
if (visible.value && inputNode === document.activeElement) {
e.stopPropagation();
}
};
</script>
<template>
<div class="search-group">
<h2 class="logo"><GIcon class="logo-icon-svg" name="gt-logo" size="182px" color="#FC5533"></GIcon>开源搜索</h2>
<!-- <h2 class="logo">开源搜索</h2> -->
<div
class="search-input"
:class="{ 'is-focus': visible, 'focus-show-dropdown': searchList[0] && visible }"
ref="refInputGroup"
>
<d-dropdown
shift-offset
:overlay-class="`search-tool-overlay ${!searchList[0] ? 'hidden' : ''}`"
v-model="visible"
:align="'start'"
:position="['bottom-start']"
:offset="[0, 0]"
:show-animation="false"
@toggle="handleToggle"
>
<d-input
class="search-tool"
clearable
placeholder="请告诉我想要查找的开源项目…"
v-model.trim="searchValue"
size="lg"
@keyup.enter="onKeyupEnter"
@input="onInput"
@clear="handleClear"
@compositionstart="inputIng = true"
@compositionend="inputIng = false"
@click="handleInputClick"
>
<template #prefix>
<GIcon name="gt-search" class="search-icon" />
</template>
<!-- <template #append><d-button class="search-btn" variant="solid" color="var(--devui-primary, #5e7ce0)" >开搜</d-button ></template> -->
</d-input>
<template #menu>
<ul
class="search-tool-items"
:style="{
width: width + 'px'
}"
v-if="searchList[0] && visible"
>
<li class="item" v-for="item in searchList" :key="item.id" @click="handleToProject(item)">
<div class="option-name">{{ item.name || '-' }}</div>
<div class="option-desc">
{{ item.description || '暂无简介' }}
</div>
</li>
</ul>
</template>
</d-dropdown>
<span class="split"></span>
<d-button class="search-btn" size="lg" @click="goSearch">开搜</d-button>
</div>
<div class="search-history">
搜索历史
<d-popover
content="全部清空"
:position="['top']"
trigger="hover"
:mouse-enter-delay="500"
style="background-color: #fff; color: #2d2d2e"
>
<GIcon
class="cursor-pointer"
name="gt-delete"
color="var(--color-CG600,#707A87)"
@click="searchHistoryList = []"
></GIcon>
</d-popover>
</div>
<div class="search-history-searchList">
<d-tag
size="lg"
class="history-tag"
v-for="(item, index) in searchHistoryList"
:key="index"
:deletable="item.deletable"
@click="goSearch(item.value)"
>
<span :title="item.value" class="tag-name">{{ item.value }}</span>
</d-tag>
</div>
</div>
</template>
<style lang="scss">
$borderWidth: 2px;
@mixin primary-border {
border: $borderWidth solid var(--devui-primary, #5e7ce0);
}
@mixin default-border {
border: $borderWidth solid var(--color-CG400, #f0f2f7);
}
.search-group {
height: 540px;
.logo {
height: calc(50% - 44px);
font-size: 42px;
font-family: NotoSansSC, NotoSansSC;
font-weight: bold;
padding-top: 119px;
padding-bottom: 46px;
@apply text-CG800 text-center flex items-center justify-center gap-x-[8px];
.logo-icon-svg {
height: 42px;
vertical-align: middle;
}
&::before {
height: 40px;
width: 145.829596px;
// content: '';
// background-image: url('@/assets/imgs/logo/gitcode-logo.png');
@apply inline-block bg-no-repeat bg-center bg-cover align-[-0.125em];
}
}
// 输入区域
.search-input {
@include default-border;
border-radius: 6px; // var(--devui-border-radius, 2px);
@apply flex mx-auto max-w-[780px];
.devui-input {
height: 44px;
}
.devui-input__inner::placeholder {
color: var(--color-CG600);
}
.devui-input__wrapper {
@apply border-0 pl-[12px] rounded-[6px];
}
.devui-input__wrapper:hover {
@apply border-0;
}
.devui-input__wrapper:not(.devui-input--error):not(.devui-input--disabled):not(.devui-input--focus):hover {
@apply border-0;
}
.devui-input-slot__append {
@apply border-0;
}
// 聚焦时处理输入框样式
&.is-focus {
@include primary-border;
.split {
&:before {
background-color: var(--devui-primary, #5e7ce0);
}
}
}
// 聚焦时有下拉框
&.focus-show-dropdown {
@include primary-border;
.devui-input__wrapper {
@apply rounded-bl-[0] rounded-tr-[0] rounded-br-[0] border-r-0 border-b-0;
}
.devui-input__wrapper.devui-input--focus {
@apply border-b-0 rounded-bl-[0] rounded-br-[0];
}
.search-btn {
@apply border-b-0 border-l-0 rounded-br-[0];
}
.split {
&:before {
background-color: var(--devui-primary, #5e7ce0);
}
}
}
.search-btn {
height: 44px;
background-color: #fff;
@include default-border;
@apply text-G900 rounded-tl-[0] rounded-bl-[0] border-0 rounded-[6px];
}
.split {
@apply bg-white flex justify-center items-center;
&:before {
display: block;
content: '';
width: 2px;
height: 24px;
background-color: var(--color-CG400);
}
}
}
.search-icon {
color: #9fa7b3 !important;
font-size: 18px !important;
}
// 搜索历史
.search-history {
font-weight: 500;
@apply text-center text-sm text-CG600 mt-8 leading-[20px] flex justify-center items-center gap-4;
&-searchList {
animation: show 1s;
@keyframes show {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@apply text-center mt-[12px] text-ellipsis line-clamp-2 overflow-hidden;
.history-tag {
@apply mr-2 mb-2;
&:last-of-type {
@apply mb-0 mr-0;
}
.devui-tag__item {
font-size: 14px;
font-family: NotoSansSC, NotoSansSC;
font-weight: 500;
background-color: var(--color-CG300, #e4e9f0);
// padding-left: 32px;
// padding-right: 32px;
transition: padding ease 0.1s;
@apply max-w-[300px] text-G900 text-sm cursor-pointer overflow-hidden text-ellipsis whitespace-nowrap;
&:hover {
@apply text-CG600;
}
.tag-name {
@apply text-ellipsis overflow-hidden inline-block max-w-[100%];
}
}
&.devui-tag .devui-tag__item.devui-tag--lg {
height: 36px;
line-height: 34px;
@apply py-[2] px-4;
}
}
}
}
@media screen and (max-width: 576px) {
height: 340px;
.logo {
padding-top: 30px;
}
}
}
// 搜索下拉框
.search-tool-overlay {
box-shadow: none;
@include primary-border;
transform: translateX(-$borderWidth) translateY(-$borderWidth);
@apply border-t-0 rounded-tl-[0] rounded-tr-[0] rounded-[6px];
}
.search-tool-items {
padding-left: 12px;
padding-right: 12px;
height: 100%;
max-height: 380px;
overflow-y: auto;
.item {
padding: 12px 0px;
border-top: 1px solid var(--color-CG300, #e4e9f0);
@apply leading-[20px] overflow-hidden text-ellipsis cursor-pointer;
}
.option-name {
font-family: NotoSansSC, NotoSansSC;
font-weight: 500;
@apply text-G900 overflow-hidden text-ellipsis whitespace-nowrap;
}
.option-desc {
@apply text-CG600 overflow-hidden text-ellipsis whitespace-nowrap;
}
}
</style>

48
src/views/Intro/index.vue Normal file
View File

@@ -0,0 +1,48 @@
<template>
<div id="home-container"></div>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeMount } from 'vue';
import { useRouter } from 'vue-router';
import emitter from '@/utils/eventBus';
import microApp from '@micro-zoe/micro-app';
const router = useRouter();
const toolbarContain = ref<any>(null);
const origin = (import.meta as any).env.VITE_CHILD_HOMEWEB_HOST;
onMounted(() => {
toolbarContain.value = document.getElementById('toolbarBottom');
microApp.renderApp({
name: 'micoro-app-homeweb-app',
url: origin,
container: '#home-container',
data: {
emitter,
toolbarContain,
$router: router
},
iframe: true,
baseroute: '/'
});
});
onBeforeMount(() => {
microApp.unmountApp('micoro-app-homeweb-app');
});
</script>
<style lang="scss" scoped>
.intro-page {
width: 100%;
}
.intro-page-mobile {
:deep(.intro-banner) {
background: url(@/assets/imgs/home/mobile-banner.png);
background-repeat: no-repeat, no-repeat;
background-size: contain;
background-position: center center;
}
}
</style>

44
src/views/Intro/types.ts Normal file
View File

@@ -0,0 +1,44 @@
export type dynamicListRow = {
avatar_url?: string,
author?: string,
remand?: string,
time?: string
}
export type activeOrganizationRow = {
avatar_url?: string,
name?: string,
description?: string,
members?: string,
full_path?: string,
[propName:string]: any
}
export type selectedReposListRow = {
id?: string,
iconList?: Array<any>,
title?: string,
tag?: string,
desc?: string,
imgUrl?: string,
path_with_namespace?: string,
[propName: string]: any;
}
export type bannerType = {
imgUrl?: string
link_to?: string
}
export type communityType = {
img_url?: string,
title?: string,
link_to?: string,
desc?: string
}
export type operationDataType = {
img_url?: string,
link_to?: string,
}