搜索结果列表页面开发
This commit is contained in:
166
src/views/Search/components/group.vue
Normal file
166
src/views/Search/components/group.vue
Normal 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>
|
||||
193
src/views/Search/components/repos.vue
Normal file
193
src/views/Search/components/repos.vue
Normal 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>
|
||||
130
src/views/Search/components/users.vue
Normal file
130
src/views/Search/components/users.vue
Normal 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>
|
||||
50
src/views/Search/index.vue
Normal file
50
src/views/Search/index.vue
Normal 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>
|
||||
Reference in New Issue
Block a user