搜索结果列表页面开发

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,166 @@
<script setup lang="ts">
import { ref, reactive, nextTick, watchEffect, onBeforeUnmount } from 'vue';
import { useRoute } from 'vue-router';
import { highlightWords } from '@/utils';
import { getFuzzyGroups } from '@/api/org';
const route = useRoute();
const groups = ref<any[]>([1, 2, 3, 4]);
const props = defineProps<{ orderBy: string }>();
const pager = reactive({
total: 0,
pageSize: 10,
pageIndex: 1
});
const loading = ref(true);
const emits = defineEmits(['updateTotal']);
const getData = async() => {
const keywords = route?.query?.val as string;
if (!loading.value) loading.value = true;
const sortParam = props.orderBy.split(',');
const res = await getFuzzyGroups({
name: keywords?.toString() || '',
path: keywords?.toString() || '',
description: keywords?.toString() || '',
page: pager.pageIndex,
per_page: pager.pageSize,
orderBy: sortParam[0].trim() as any,
sort: sortParam[1].trim() as any
});
if (!res.error) {
groups.value = [...res.data.data.content];
pager.total = Number(res.data.data.total);
}
loading.value = false;
emits('updateTotal', pager.total);
};
const handleIndexChange = async() => {
await getData();
nextTick(() => emits('updateTotal', pager.total));
};
const handleSizeChange = async() => {
pager.pageIndex = 1;
await getData();
nextTick(() => emits('updateTotal', pager.total));
};
const handleClick = (row: any) => {
if (row?.web_url) {
window.open(row.web_url);
} else {
window.open('404');
}
};
const handleSearch = (_: string) => {
const timer = setTimeout(() => {
getData();
}, 100);
const cancel = () => {
timer && clearTimeout(timer);
};
return {
cancel
};
};
const stop = watchEffect((onCleanup) => {
const { cancel } = handleSearch(props.orderBy);
onCleanup(cancel);
});
onBeforeUnmount(() => {
stop();
});
</script>
<template>
<div class="page-search-group">
<div v-loading="groups?.length ? loading : false">
<DataPanel :empty="!groups?.length" :loading="loading" skeleton :card="false">
<div class="page-search-group-item" v-for="item of groups" :key="item.id">
<GAvatar
:src="item.avatar"
:name="item.name || 'Unkonw'"
:width="64"
:height="64"
:is_round="false"
class="mr-5 cursor-pointer"
@click="handleClick(item)"
></GAvatar>
<div class="flex-1 overflow-hidden leading-[20px]">
<div class="inline-flex text-[14px] items-center w-[100%] my-4">
<div class="text-G900 font-[500] mr-2 cursor-pointer" v-html="highlightWords(route.query.val as string, item.name || 'unkown')" @click="handleClick(item)"></div>
<a class="text-CG600 truncate max-w-[60%] block" :href="item.web_url || '404'" target="_blank" v-html="highlightWords(route.query.val as string, `@${item.path || 'unkown'}`)"></a>
</div>
<div class="text-[14px] text-CG600 leading-[20px] h-[40px] truncate" v-html="highlightWords(route.query.val as string, item.description || '暂无简介')"></div>
<!-- <div class="flex items-center">
<div class="inline-flex items-center mr-4">
<Icon name="gt-folder" class="w-4 h-4"/>
<span class="inline-block pl-1 text-CG600">{{ Number(item.project_count) || 0 }}</span>
</div>
<div class="inline-flex items-center mr-4">
<Icon name="gt-member" class="w-4 h-4"></Icon>
<span class="inline-block pl-1 text-CG600">{{ Number(item.members) || 0 }}</span>
</div>
</div> -->
</div>
</div>
</DataPanel>
</div>
<div class="page-search-group-pagination" v-show="pager.total > pager.pageSize">
<d-pagination
size="md"
:page-size-options="[10, 20, 50]"
:show-page-selector="false"
:can-view-total="true"
:can-change-page-size="true"
:max-items="5"
:total="pager.total"
v-model:pageSize="pager.pageSize"
v-model:pageIndex="pager.pageIndex"
:max-page="500"
@page-index-change="handleIndexChange"
@page-size-change="handleSizeChange"
/>
</div>
</div>
</template>
<style lang="scss">
.page-search-group {
&-pagination {
padding-top: 20px;
padding-bottom: 20px;
display: flex;
justify-content: center;
}
&-item {
height: 100px;
box-sizing: border-box;
border-style: solid;
border-color: var(--color-G200);
border-bottom-width: 1px;
display: flex;
align-items: center;
padding: 18px;
// &:last-of-type {
// border-width: 0;
// }
a:hover {
color: var(--color-CG600);
}
&-ellipsis {
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2; /* 设置文本最大行数为2行 */
-webkit-box-orient: vertical;
text-overflow: ellipsis;
white-space: normal; /* 在Safari中需要添加这一行来处理部分情况 */
}
}
}
</style>

View File

@@ -0,0 +1,193 @@
<template>
<div class="page-search-repo" >
<div v-loading="createdRepoList?.length ? loading : false" >
<DataPanel :empty="!createdRepoList?.length" :loading="createdRepoList?.length ? false : loading" skeleton :card="false" >
<div v-for="(item, index) in createdRepoList" :key="item.id" class="search-repo-item">
<repo-item
:iconHandleList="item.iconHandleList"
:id="item.id"
:imgSrc="item.imgSrc"
:title="item.title"
:tag-list="item.tagList"
:desc="item.desc"
:isStar="item.isStar"
:web_url="item.web_url"
:keywords="searchVal"
:topic-names="item.topicNames"
:class="{ 'is-last': index === createdRepoList.length - 1 }"
@handle-star="({isStar}) => item.isStar = isStar"
>
<template #name>
<div class="text-base text-G900 cursor-pointer break-all inline" @click="gotoRepo(item)">
<template v-for="(ns,index) in item.namePath" :key="index">
<span :class="index<item.namePath.length-1?'font-[600]':''" v-html="highlightWords(searchVal,ns)"></span>
<span v-if="index<item.namePath.length-1" class="mx-1">/</span>
</template>
</div>
</template>
<template #bottom>
<div class="mt-3 repo-foot text-CG600 gap-2 flex items-center">
<d-button @click="handleDownload(item)">下载源码</d-button>
<d-button @click="handleViewSource(item)">查看源码</d-button>
<d-popover content="敬请期待" pop-type="info" :position="['bottom']"><d-button>一键部署</d-button></d-popover>
<d-popover content="敬请期待" pop-type="info" :position="['bottom']"><d-button>体验实例</d-button></d-popover>
</div>
</template>
</repo-item>
</div>
</DataPanel>
</div>
<div class="page-search-repo-pagination" v-show="pager.total > pager.pageSize">
<d-pagination
size="md"
:page-size-options="[10, 20, 50]"
:show-page-selector="false"
:can-view-total="true"
:can-change-page-size="true"
:max-items="5"
:total="pager.total"
v-model:pageSize="pager.pageSize"
v-model:pageIndex="pager.pageIndex"
:max-page="100"
@page-index-change="handleIndexChange"
@page-size-change="handleSizeChange"
/>
</div>
</div>
</template>
<script setup lang="ts">
import RepoItem, { dataHandler } from '@/components/RepoItem/index.vue';
import type { RepoItemResData } from '@/components/RepoItem/types';
import { ref, watch, nextTick } from 'vue';
import { useUserInfo } from '@/views/User/hooks/useUserInfo';
import { useLoginCheck } from '@/utils/hooks/useLoginCheck';
import { getRepoSearch, starRepo, unstarRepo } from '@/api/repo';
import { useRoute, useRouter } from 'vue-router';
import { Message } from 'vue-devui/message';
import { highlightWords } from '@/utils';
import { emitEvent } from '@/utils/eventBus';
import { useReport } from '@/utils/hooks/useReport';
const route = useRoute();
const router = useRouter();
const { loginCheck } = useLoginCheck();
const props = defineProps<{ orderBy: string }>();
const queryVal = route.query?.val;
const fileDownloadBaseURL = (import.meta as any).env.VITE_DOWNLOAD_HOST;
const searchVal = typeof (queryVal) === 'string' ? queryVal : '';
const pager = ref({
total: 0,
pageSize: 10,
pageIndex: 1
});
const emits = defineEmits(['updateTotal']);
const { namespace, userInfo } = useUserInfo();
const handleViewSource = (item:any) => { // 浏览源码
window.open(`/${item.path_with_namespace}/tree/${item.default_branches}`, '_blank');
};
const handleWait = ref<any>(null);
const downLoadWait = ref(30); // 文件频繁下载间隔
function start() { // 倒计时
handleWait.value = setInterval(() => {
downLoadWait.value--;
if (downLoadWait.value <= 0) {
clearInterval(handleWait.value);
handleWait.value = null;
downLoadWait.value = 30;
}
}, 1000);
}
const reportClone = (params:any) => {
useReport('clone_repo_fr', {
repo_title: params.name,
repo_namespace: params.namespace,
namespace_type: 'group',
clone_url: params.url,
type: params.cloneType,
file_type: params.fileType
});
};
const handleDownload = (item:any) => { // 下载源码
if (handleWait.value) {
Message.warning(`文件频繁下载,请${downLoadWait.value}s后再试`);
return;
}
start();
const url = `${fileDownloadBaseURL}/${item.id}/archive/refs/heads/${item.default_branches}.zip`;
if (loginCheck('下载源码')) {
// 上报
reportClone({ ...item, url, cloneType: 'download', fileType: 'zip' });
window.open(url, '_self');
}
};
const createdRepoList = ref<RepoItemResData[]>([]);
const createdTotal = ref(0);
const toggleRepoStar = ({ id, isStar }) => { // 用户star
if (!userInfo || !userInfo.username) {
emitEvent('login');
return false;
}
if (isStar) {
unstarRepo({ repoId: id }).then(() => { search(); });
} else {
starRepo({ repoId: id }).then(() => { search(); });
}
};
const search = async() => { // 获取项目查询列表
const res = await getRepoSearch({
search: searchVal,
sort: props.orderBy,
page: pager.value.pageIndex,
per_page: pager.value.pageSize
});
if (res.data) {
const { total, content } = res.data.data;
createdTotal.value = total;
createdRepoList.value = dataHandler(content || []);
}
};
const gotoRepo = (item:any) => {
window.open(item.to, '_blank');
};
watch(createdTotal, (val) => {
pager.value.total = val || 0;
emits('updateTotal', pager.value.total);
}, { immediate: true });
watch(() => props.orderBy, () => {
search();
}, { deep: true, immediate: true, flush: 'post' });
const handleIndexChange = () => {
search();
};
const handleSizeChange = () => {
pager.value.pageIndex = 1;
nextTick(() => {
search();
});
};
</script>
<style scoped lang="scss">
.page-search-repo {
&-pagination {
padding-top: 20px;
padding-bottom: 20px;
display: flex;
justify-content: center;
}
.search-repo-item{
padding-top: 16px;
&:first-child{padding-top: 0px;}
}
:deep(.g-repo-item){
padding: 0px;
padding-bottom: 16px;
}
}
</style>

View File

@@ -0,0 +1,130 @@
<template>
<div class="page-user">
<DataPanel :empty="!data?.length" :loading="loading" skeleton :card="false">
<div class="page-user-content ">
<user-follow-list :data="data" :keywords="searchVal" :show-button="false" @refresh="onRefresh" />
</div>
</DataPanel>
<div class="page-user-pagination" v-if="pager.total > pager.pageSize">
<d-pagination
:total="pager.total"
v-model:pageSize="pager.pageSize"
v-model:pageIndex="pager.pageIndex"
:can-view-total="true"
:can-change-page-size="true"
:max-items="5"
:max-page="500"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import UserFollowList from '@/views/User/components/user-follow-list.vue';
import { searchUserList } from '@/api/user';
import { useUserInfo } from '@/views/User/hooks/useUserInfo';
import type { FollowUserData } from '@/views/User/components/types';
import { useRouter, useRoute } from 'vue-router';
const route = useRoute();
const { val: searchVal } = route.query as any;
const { namespace, isSelf } = useUserInfo();
const pager = ref({
total: 0,
pageSize: 10,
pageIndex: 1
});
const data = ref<FollowUserData[]>([]);
const loading = ref(false);
const userInfoComp = ref<any>(null);
const emits = defineEmits(['updateTotal']);
const init = (params: { pageNum: number; pageSize: number; }) => {
if (loading.value) return false;
loading.value = true;
searchUserList({ keyword: searchVal || '', ...params })
.then((res) => {
const resData = res.data || { content: [], total: 0 };
const innerData = resData.content || [];
pager.value.total = resData.total || 0;
data.value = innerData.map((item) => {
return {
id: item.user_id || '',
avatar: item.avatar || '',
nickname: item.nickname || '',
username: item.username || '',
description: item.profile.description || '',
location: item.profile.location || '',
// org: item.groupname || '',
loading: false,
website: item.profile.website || ''
// hasStarred: !!item.is_friend
};
});
emits('updateTotal', pager.value.total);
})
.catch((err) => {
if (err && Number(err.error_code) === 404) {
const router = useRouter();
router.push({ name: '404' });
}
})
.finally(() => {
loading.value = false;
});
};
init({ pageNum: pager.value.pageIndex, pageSize: pager.value.pageSize });
const onRefresh = () => {
if (userInfoComp.value) {
userInfoComp.value.getAllCounts();
}
init({ pageNum: pager.value.pageIndex, pageSize: pager.value.pageSize });
};
watch(() => pager.value.pageIndex, () => {
onRefresh();
}, { flush: 'post' });
watch(() => pager.value.pageSize, () => {
onRefresh();
}, { flush: 'post' });
</script>
<style lang="scss" scoped>
.page-user {
&-content {
border: none;
}
&-pagination {
padding-top: 20px;
padding-bottom: 20px;
display: flex;
justify-content: center;
}
:deep(.user-follow-list) {
box-shadow: none;
border-radius: 0;
.g-user-item{
padding-left:0px;
padding-right: 0px;
&:first-child{padding-top: 0px;}
}
.devui-avatar{
img{
width: 48px !important;
height: 48px !important;
}
.devui-avatar--style{
width: 48px !important;
height: 48px !important;
line-height: 48px !important;
}
}
}
}
</style>

View File

@@ -0,0 +1,50 @@
<template>
<div id="homeweb-container"></div>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import emitter from '@/utils/eventBus';
import microApp from '@micro-zoe/micro-app';
const route = useRoute();
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: '#homeweb-container',
data: {
emitter,
toolbarContain,
$router: router
},
iframe: true,
baseroute: '/',
defaultPtah: route.path
});
});
onBeforeUnmount(() => {
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>