搜索结果列表页面开发

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,56 @@
<script setup lang="ts">
import EventAdapter from './eventAdapter.vue';
const props = defineProps<{
repoInfo: any;
loadingStatus: {
profileLoading: boolean;
readmeLoading: boolean;
eventsLoading: boolean;
contributorLoading: boolean;
releasesLoading: boolean;
};
isMobile?:boolean;
timeData: any[];
loadMoreConfig: {
loadMore: boolean;
loadMoreText: string;
};
}>();
defineEmits<{(evnet: 'loadMore'): void }>();
</script>
<template>
<div>
<div class="repo-activity g-card flex flex-col pb-1" :class="isMobile ? '' : 'absolute-full' " style="overflow: hidden;">
<div class="text-CG800 flex items-center px-5 py-10 bg-white gap-5 font-bold leading-0 g-border-light border-b-[1px]">
<Icon name="gt-calendar-c" />项目动态
</div>
<d-skeleton :loading="loadingStatus.eventsLoading" :rows="10" class="p-5">
<div class="overflow-auto p-5 pt-4 flex flex-1 flex-col gap-5" :class="isMobile?'max-h-[400px]':''">
<div v-for="item in timeData" :key="item.action" class="event-item">
<EventAdapter
:eventData="item"
:fullPath="repoInfo.web_url"
:namespace="repoInfo.namespace"
></EventAdapter>
</div>
<div class="flex justify-center items-center py-2 mb-5" v-if="timeData.length">
<span
v-if="loadMoreConfig.loadMore"
class="text-link cursor-pointer"
@click="$emit('loadMore')"
>加载更多</span
>
<span v-else class="text-G400">已经到底啦</span>
</div>
</div>
</d-skeleton>
</div>
</div>
</template>
<style scoped lang="scss">
@import 'devui-theme/styles-var/devui-var.scss';
</style>

View File

@@ -0,0 +1,72 @@
<script setup lang="ts">
import { useNav } from '@/utils/hooks/useNav';
import { ref, inject } from 'vue';
const { naviTo } = useNav();
import { useRouter } from 'vue-router';
import { checkUsername } from '@/api/user';
const isCheckUser = ref(true);
const router = useRouter();
const checkUser = async(name: string) => {
const res = await checkUsername(name || '');
if (!res.error) {
if (res.data.data.result === false) {
isCheckUser.value = false;
}
} else {
isCheckUser.value = false;
}
};
const p = defineProps<{
contributorList: any[];
loadingStatus: any;
}>();
const isDeveloper = inject('isDeveloper');
const showNum = 20;
const to = (x) => x.username && router.push({ name: 'homepage', params: { namespace: x.username }});
</script>
<template>
<div class="repo-contributor flex flex-col mb-5 g-card">
<div class="text-CG800 px-5 py-10 font-medium flex items-center justify-between g-border-light border-b-[1px]">
<div class="flex items-center font-bold gap-2">
<Icon name="gt-member-c" class="mr-3" />贡献者
<span
class="text-CG600 font-normal"
:class="{ 'cursor-pointer': isDeveloper }"
v-if="contributorList.length > 0"
@click="isDeveloper ? naviTo('repoMember') : null"
>
{{ contributorList.length }}
</span>
</div>
<div class="">
<Icon name="gt-all" @click="naviTo('repoContributor')" class="cursor-pointer"></Icon>
</div>
</div>
<div class="p-5 pt-4" v-loading="loadingStatus.contributorLoading">
<div v-if="!contributorList?.length" class="flex-center text-lighter flex-1">暂无数据</div>
<div v-else class="w-8 h-8 inline-block -mr-1" v-for="(avt, idx) in contributorList.slice(0, showNum)" :key="idx">
<GAvatar
:name="avt.name"
:src="avt.avatar"
:title="avt.name"
@click="to(avt)"
class="w-8 h-8 rounded-full shadow max-w-none"
:class="{ 'cursor-pointer': avt.username }"
/>
</div>
<div
v-if="contributorList.length > showNum"
@click="naviTo('repoContributor')"
class="ml-4 inline-block align-top leading-8 cursor-pointer hover:text-link"
>
<span>+ {{ contributorList.length - showNum }} 贡献者</span>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
@import 'devui-theme/styles-var/devui-var.scss';
</style>

View File

@@ -0,0 +1,122 @@
<template>
<div v-if="['xxl', 'xl', 'md'].includes(widthType)">
<div>
<GIcon name="gt-plane-upload-black" />
<span class="text-[#000] text-[16px] ml-2 font-semibold">下载使用量</span>
</div>
<div class="flex mt-4 items-end">
<div class="text-[#2951E0] text-xl mr-8 font-semibold">
{{ total }}
</div>
<div class="num-empty" v-if="isEmpty"></div>
<div v-else id="download" style="width: 100%; height: 56px"></div>
</div>
</div>
<div v-else class="p-2 download-chart-mobile">
<div class="mb-4">
<span class="text-[#000] text-[16px] ml-2 font-semibold">下载使用量</span>
<span class="text-[#2951E0] text-[16px] font-semibold">
{{ total }}
</span>
</div>
<div v-if="isEmpty" class="num-empty"></div>
<div v-else id="download" style="width: 100%; height: 56px"></div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref, nextTick } from 'vue';
import { getRepoClone } from '@/api/repo';
import { usePageResize } from '@/utils/hooks/usePageResize';
import * as echarts from 'echarts';
const props = defineProps<{
repoId: string;
}>();
const { widthType } = usePageResize();
const isEmpty = ref(false);
const initGitCode = () => {
const myChart = echarts.init(document.getElementById('download'));
myChart.setOption({
xAxis: {
type: 'category',
show: false,
boundaryGap: false
},
yAxis: {
type: 'value',
show: false
},
tooltip: {
trigger: 'axis',
renderMode: 'richText',
formatter: function(params) {
return `${params[0].value}`;
}
},
grid: {
left: 0,
bottom: 2,
top: 0,
right: 0,
height: 50
},
series: [
{
data: downloadList.value,
type: 'line',
symbol: 'none',
lineStyle: { color: '#2951E0' },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(41, 81, 224, 0.24)' },
{ offset: 1, color: 'rgba(41, 81, 224, 0.04)' }
])
}
}
]
});
};
const downloadList = ref([]);
const total = ref(0);
const getDownloadData = () => {
getRepoClone(props.repoId).then((data) => {
if (data?.data?.data) {
total.value = data.data.data[0]?.total_dl_cnt || 0;
data.data.data.forEach((item) => {
downloadList.value.unshift(item.today_dl_cnt || 0);
});
}
if (total.value === 0) {
isEmpty.value = true;
} else {
// 只有一天数据前面加个0展示折线
downloadList.value.length === 1 && (downloadList.value.unshift(0));
nextTick(() => {
initGitCode();
});
}
});
};
onMounted(() => {
getDownloadData();
});
</script>
<style lang="scss" scoped>
.download-chart-mobile {
border-radius: 4px;
border: 1px solid #E3E3EE;
background: #F1F1F8;
}
.num-empty {
height: 10px;
border-top: 2px solid #2951E0;
width: 100%;
background: linear-gradient(180deg, rgba(41, 81, 224, 0.12) 0%, rgba(41, 81, 224, 0.04) 100%);
}
</style>

View File

@@ -0,0 +1,33 @@
<script setup lang="ts">
import { useRouter } from 'vue-router';
const router = useRouter();
defineProps({
forkCount: {
default: 0
}
});
</script>
<template>
<div class="fork-button flex items-center px-3.5 cursor-pointer">
<div @click="router.push({ name: 'repoForkCreate' })" class="flex items-center">
<Icon name="gt-fork" class="mr-2"></Icon>
<span class="text-G900 mr-2">Fork</span>
</div>
<span class="text-CG600 hover:text-link" @click="router.push({ name: 'repoFork' })">
<Number :number="forkCount || 0" />
</span>
</div>
</template>
<style scoped lang="scss">
@import 'devui-theme/styles-var/devui-var.scss';
.fork-button {
height: 32px;
background: var(--color-linear100);
border-radius: 3px;
border: 1px solid var(--color-border);
}
</style>

View File

@@ -0,0 +1,113 @@
<script setup lang="ts">
import { useNav } from '@/utils/hooks/useNav';
import { toRef } from 'vue';
import { useRoute } from 'vue-router';
import MdRender from '@/components/MdRender/index.vue';
const { naviTo } = useNav();
const props = defineProps<{
repoInfo: any;
loadingStatus: {
profileLoading: boolean;
readmeLoading: boolean;
eventsLoading: boolean;
contributorLoading: boolean;
releasesLoading: boolean;
};
content: string;
isOver: boolean;
showMore: boolean;
editable: boolean;
}>();
const mdContent = toRef(props.content);
defineEmits<{(event: 'loadMore'): void }>();
// /// 相对路径转绝对路径
const fileBaseURL = (import.meta as any).env.VITE_DOWNLOAD_HOST;
const orgin = window.location.origin;
const { params: { namespace, repoName }} = useRoute();
const branchName = props.repoInfo.default_branch;
const imgPrefix = `${fileBaseURL}/${Array.isArray(namespace) ? namespace.join('/') : namespace}/${repoName}/files/${branchName}`;
const filePrefix = `${orgin}/${Array.isArray(namespace) ? namespace.join('/') : namespace}/${repoName}/blob/${branchName}`;
const dirPrefix = `${orgin}/${Array.isArray(namespace) ? namespace.join('/') : namespace}/${repoName}/tree/${branchName}`;
const customPlugins = [{
pluginName: 'linkCovertPlugin', // 插件名字(固定名称)
opts: {
imageSrcCovert: relativeUrl => `${imgPrefix}/${relativeUrl}`,
linkUrlCovert: relativeUrl => {
// 文件/文件夹路径转换
if (relativeUrl?.split('/')?.pop()?.includes('.')) {
return `${filePrefix}/${relativeUrl}`; // 文件(非文件夹)
}
return `${dirPrefix}/${relativeUrl}`;
}
}
}];
</script>
<template>
<Card simple class="markdown-card box mb-5">
<div class="warp-repo-readme relative pb-1">
<div class="readme-header flex items-center justify-between border-b border-G200 px-5 py-2.5">
<div class="flex items-center">
<Icon name="gt-file2-c"></Icon><span class="ml-5 font-bold">README.md</span>
</div>
<div
v-if="editable"
@click="naviTo('editRepoFile', {branchName,filePath: 'README.md'})"
class="cursor-pointer"
title="编辑readme"
>
<Icon name="gt-edit"></Icon>
</div>
</div>
<div
ref="eleRef"
class="repo-file-markdown"
:class="{ 'show-all': showMore }"
v-loading="loadingStatus.readmeLoading"
>
<MdRender v-model="mdContent" class="p-4 min-h-[100px]" :custom-plugins="customPlugins"></MdRender>
<div v-if="isOver && !showMore" class="repo-file-markdown-btn" @click="$emit('loadMore')">
查看全部
</div>
</div>
</div>
</Card>
</template>
<style scoped lang="scss">
@import 'devui-theme/styles-var/devui-var.scss';
.warp-repo-readme {
:deep(.dp-md-container) {
border: none;
.dp-editor-md-preview-container {
height: auto;
}
}
}
.repo-file-markdown {
position: relative;
max-height: 400px;
overflow: auto;
&.show-all {
max-height: 100%;
}
&-btn {
position: absolute;
left: 50%;
bottom: 0;
transform: translateX(-50%);
z-index: 2;
width: 100%;
text-align: center;
background: #fff;
&:hover {
cursor: pointer;
}
}
}
</style>

View File

@@ -0,0 +1,106 @@
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps({
modelValue: {
default: ''
},
watchCount: {
default: 0
}
});
const emit = defineEmits<{(event: 'update:modelValue', value: string): void;
(event: 'change', value: string): void;
}>();
const status = computed({
get() {
return props.modelValue;
},
set(value: string) {
emit('update:modelValue', value);
}
});
const noticeMap = new Map([
['default', '仅我参与的'],
['all', 'Watching'],
['ignore', '忽略通知']
]);
const noticeIconMap = new Map([
['default', 'gt-remind-me'],
['all', 'gt-remind'],
['ignore', 'gt-remind-forbid']
]);
</script>
<template>
<d-dropdown :position="['bottom-end']" align="start">
<div class="notice-status flex items-center px-3.5 cursor-pointer">
<Icon :name="noticeIconMap.get(modelValue) || 'gt-remind'" class="mr-1" />
<span class="text-G900 mr-2">{{ noticeMap.get(modelValue) }}</span>
<span class="text-CG600 mr-2">
<Number :number="watchCount || 0" />
</span>
<i class="icon-select-arrow"></i>
</div>
<template #menu>
<div class="w-60 rounded-sm p-3">
<div class="pop-head flex items-center justify-between">
<div class="flex items-center">
<Icon name="gt-remind" class="mr-1" /><span>通知设置</span>
</div>
<div class="cursor-pointer">
<Icon name="gt-close" />
</div>
</div>
<div class="mt-2.5" @click.stop>
<d-radio-group
:modelValue="status"
@update:modelValue="status = $event"
@change="emit('change', $event)"
>
<div class="p-2 border border-G200 rounded-sm mb-2 w-full">
<d-radio value="default">
<div>
<p class="font-medium text-G900">仅我参与的</p>
<p class="text-xs text-CG600 mt-1">
只有当我参与或被@提及时才接收来自此项目的通知
</p>
</div>
</d-radio>
</div>
<div class="p-2 border border-G200 rounded-sm mb-2 w-full">
<d-radio value="all">
<div>
<p class="font-medium text-G900">全部通知</p>
<p class="text-xs text-CG600 mt-1">接收来自此项目上的所有通知</p>
</div>
</d-radio>
</div>
<div class="p-2 border border-G200 rounded-sm w-full">
<d-radio value="ignore">
<div>
<p class="font-medium text-G900">忽略通知</p>
<p class="text-xs text-CG600 mt-1">从不接收任何来自此项目的通知</p>
</div>
</d-radio>
</div>
</d-radio-group>
</div>
</div>
</template>
</d-dropdown>
</template>
<style scoped lang="scss">
@import 'devui-theme/styles-var/devui-var.scss';
.notice-status {
height: 32px;
background: var(--color-linear100);
border-radius: 3px;
border: 1px solid var(--color-border);
}
</style>

View File

@@ -0,0 +1,62 @@
<script setup lang="ts">
import { useNav } from '@/utils/hooks/useNav';
import { useTimeFormat } from '@/utils/hooks/useTimeFormat';
const { naviTo } = useNav();
defineProps<{
releasesList: any[];
loadingStatus: any;
canAdd: boolean;
}>();
const { formatTimeFromNow } = useTimeFormat();
</script>
<template>
<div class="repo-versions g-card h-full flex flex-col overflow-hidden">
<div class="text-CG800 font-medium flex items-center justify-between px-5 py-10 g-border-light border-b-[1px]">
<div class="flex items-center gap-2">
<Icon name="gt-tag-c"></Icon>
<span class="font-bold ml-3">发行版</span>
<span
class="text-CG600 cursor-pointer"
v-if="releasesList.length > 0"
@click="naviTo('repoRelease')"
>
{{ releasesList.length }}
</span>
</div>
<div class="flex">
<Icon
v-if="canAdd"
name="gt-add"
class="hover:text-link cursor-pointer"
@click="naviTo('repoReleaseCreate')"
>
</Icon>
<Icon name="gt-all" class="ml-5 cursor-pointer" @click="naviTo('repoRelease')"></Icon>
</div>
</div>
<DataPanel
:loading="loadingStatus.releasesLoading"
:empty="!releasesList?.length"
skeleton
:card="false"
class="h-full"
>
<div class="p-5 pt-4 pb-1 flex-1 overflow-auto">
<div
v-for="(rls, idx) in releasesList"
:key="idx"
class="hover:text-link cursor-pointer flex justify-between"
@click="naviTo('repoReleaseDetail', { tagName: rls.tag_name })"
>
<span class="font-bold break-all basis-[200px]">{{ rls.name }}</span>
<span class="text-CG500 ml-4 hover:text-link">
发布于&nbsp;&nbsp;{{ formatTimeFromNow(rls.created_at) }}
</span>
</div>
</div>
</DataPanel>
</div>
</template>

View File

@@ -0,0 +1,209 @@
<template>
<d-modal :show-close="false" :draggable="false" class="repo-clone-modal rounded-md w-[90%] max-w-[600px]">
<template #header>
<d-row align="middle" class="py-3 px-4 text-[#000] bg-[#F9F9FB] rounded-md">
<d-col flex="auto" class="font-semibold">
<GIcon name="gt-plane-terminal-black" class="mr-2 cursor-default" />Clone
</d-col>
<d-col class="p-2 pr-0">
<GIcon @click="emits('handleClose')" name="gt-line-close" class="cursor-pointer hover:opacity-50" />
</d-col>
</d-row>
</template>
<div class="repo-clone-modal-content">
<div class="text-[#BCBCD0] italic"># 请确保本地完成了 Git 的全局配置</div>
<d-row class="text-border" align="middle">
<d-col flex="auto">
<div class="whitespace-pre-wrap leading-6" v-html="settingContent"></div>
</d-col>
<d-col>
<GIcon name="gt-line-copy" @click="onCopy(settingContent)" />
</d-col>
</d-row>
<div class="flex mt-8 repo-clone-tab">
<div
class="repo-clone-tab-item"
@click="handleTab('https')"
:class="{ 'repo-clone-tab-active': tabId === 'https' }"
>
<span class="repo-clone-tab-text">HTTPS</span>
</div>
<div class="repo-clone-tab-item" @click="handleTab('ssh')" :class="{ 'repo-clone-tab-active': tabId === 'ssh' }">
<span class="repo-clone-tab-text">SSH</span>
</div>
</div>
<div v-if="tabId === 'https'" class="mt-4">
<p class="text-[#BCBCD0] italic"># 复制项目地址</p>
<d-row class="text-border" align="middle">
<d-col flex="auto">
<span class="font-semibold text-[#3B3E55] break-all">{{ props.repoInfo?.http_url_to_repo }}</span>
</d-col>
<d-col>
<GIcon name="gt-line-copy" @click="onCopy(props.repoInfo?.http_url_to_repo, 'https')" />
</d-col>
</d-row>
<p class="text-[#BCBCD0] mt-4 italic"># 克隆到本地</p>
<d-row class="text-border" align="middle">
<d-col flex="auto">
<span
>git clone <span class="font-semibold text-[#3B3E55] break-all">{{ props.repoInfo?.http_url_to_repo }}</span></span
>
</d-col>
<d-col>
<GIcon name="gt-line-copy" @click="onCopy(`git clone ${props.repoInfo?.http_url_to_repo}`, 'https')" />
</d-col>
</d-row>
<div class="text-[#BCBCD0] mt-4">
<p class="italic">
# 使用 HTTPS 协议时<GLink to="/setting/token-classic">配置并使用个人访问令牌</GLink
>替代登录密码进行克隆推送等操作
</p>
<p class="mt-4">Username for 'https://gitcode.com': {{ gitData.userName }}</p>
<p>
{{ `Password for 'https://${gitData.userName}@gitcode.com':` }}
<span class="text-[#3B3E55] font-semibold">#个人访问令牌</span>
</p>
</div>
</div>
<div v-if="tabId === 'ssh'" class="mt-4 text-[#BCBCD0]">
<p class="italic"># 使用 SSH 协议时请在本地生成 SSH 公钥进行克隆推送等操作</p>
<p class="mt-4 italic"># 复制项目地址</p>
<d-row class="text-border" align="middle">
<d-col flex="auto">
<span class="text-[#3B3E55] font-semibold break-all">{{ props.repoInfo?.ssh_url_to_repo }}</span>
</d-col>
<d-col>
<GIcon name="gt-line-copy" @click="onCopy(props.repoInfo?.ssh_url_to_repo, 'ssh')" />
</d-col>
</d-row>
<p class="mt-4 italic"># 生成 RSA 密钥</p>
<d-row class="text-border" align="middle">
<d-col flex="auto">
{{ `ssh-keygen -t rsa -b 2048 -C ${gitData.userEmail}` }}
</d-col>
<d-col>
<GIcon name="gt-line-copy" @click="onCopy(`ssh-keygen -t rsa -b 2048 -C ${gitData.userEmail}`)" />
</d-col>
</d-row>
<p class="mt-4 italic"># 查看 RSA 公钥并配置到 <GLink to="/setting/key-ssh">SSH key</GLink> </p>
<d-row class="text-border" align="middle">
<d-col flex="auto"> cat ~/.ssh/id_rsa.pub </d-col>
<d-col>
<GIcon name="gt-line-copy" @click="onCopy('cat ~/.ssh/id_rsa.pub')" />
</d-col>
</d-row>
<p class="mt-4 italic"># 克隆到本地</p>
<d-row class="text-border" align="middle">
<d-col flex="auto">
git clone <span class="text-[#3B3E55] font-semibold break-all">{{ props.repoInfo?.ssh_url_to_repo }}</span>
</d-col>
<d-col>
<GIcon name="gt-line-copy" @click="onCopy(`git clone ${props.repoInfo?.ssh_url_to_repo}`, 'ssh')" />
</d-col>
</d-row>
</div>
</div>
</d-modal>
</template>
<script setup lang="ts">
import { Message } from 'vue-devui/message';
import { useClipboard } from '@vueuse/core';
import { ref, reactive } from 'vue';
import { useReport } from '@/utils/hooks/useReport';
import { useAccountStore } from '@/stores/user';
const props = defineProps({ repoInfo: Object });
const emits = defineEmits(['handleClose']);
const { accountInfo } = useAccountStore();
const gitData = reactive({
userName: accountInfo?.username || 'userName',
userEmail: accountInfo?.email || 'userEmail'
});
const tabId = ref('https');
const handleTab = (tab: string) => {
tabId.value = tab;
};
const { copy } = useClipboard();
const onCopy = (val, cloneType?: string) => {
copy(val);
Message.success('复制成功');
// 上报
if (cloneType) {
const reportUrl = cloneType === 'https' ? props.repoInfo?.http_url_to_repo : props.repoInfo?.ssh_url_to_repo;
reportClone(reportUrl, cloneType);
}
};
const settingContent = `git config --global user.name ${gitData.userName}\ngit config --global user.email ${gitData.userEmail}`;
const reportClone = (url: String, cloneType: string, fileType?: string) => {
useReport('clone_repo_fr', {
repo_title: props?.repoInfo?.name,
repo_namespace: props?.repoInfo?.namespace?.name,
namespace_type: props?.repoInfo?.namespace?.kind,
clone_url: url,
type: cloneType,
file_type: fileType
});
};
</script>
<style lang="scss">
@import 'devui-theme/styles-var/devui-var.scss';
.repo-clone-modal {
.devui-modal__body {
padding: 32px 16px;
}
svg {
color: #3B3E55!important;
}
&-content {
svg {
cursor: pointer;
&:hover {
opacity: 0.5;
}
}
.repo-clone-tab {
cursor: pointer;
&-item {
padding: 6px 8px;
}
&-text {
padding: 2px 8px;
color: #3b3e55;
}
&-active {
border-bottom: 2px solid #000;
.repo-clone-tab-text {
color: #da203e !important;
font-weight: 600;
}
}
:hover {
.repo-clone-tab-text {
border-radius: 4px;
color: #000;
background-color: rgba(#bcbcd0, 0.2);
}
}
}
.g-link {
color: #2951e0;
}
.text-border {
padding: 8px;
margin-top: 8px;
border: 1px solid #f1f1f8;
border-radius: 4px;
background-color: #f9f9fb;
color: #63667f;
gap: 16px;
}
}
}
</style>

View File

@@ -0,0 +1,110 @@
<template>
<!-- 导入失败项目不展示 -->
<div v-if="repoInfo?.import_status !== 'failed'" class="flex font-color-t1">
<!-- <div v-if="isVisitorOperate" class="mr-2 clone-btn px-4 flex cursor-pointer items-center" @click="handleEnter">
<GIcon name="gt-plane-code-red" class="mr-2" />用编辑器打开
</div> -->
<div v-if="!repoInfo.empty_repo" class="clone-btn px-4 flex cursor-pointer items-center" @click="download">
<GIcon name="gt-plane-upload-red" class="mr-2" />下载zip
</div>
<div class="ml-2 clone-btn px-4 flex cursor-pointer items-center" @click="handleClone(true)">
<GIcon name="gt-plane-terminal-red" class="mr-2" />Clone
</div>
<Notification type="warning" title="下载项目" v-model="showWarn">
<div>
<span>{{ downloadWarn }}</span>
</div>
</Notification>
<CloneRepo v-model="cloneVisible" :repoInfo="repoInfo" @handleClose="handleClone(false)" />
</div>
</template>
<script lang="ts" setup>
import { onUnmounted, ref, computed } from 'vue';
import { useRoute } from 'vue-router';
import { Notification } from 'vue-devui/notification';
import CloneRepo from './CloneRepo.vue';
import { useLoginCheck } from '@/utils/hooks/useLoginCheck';
import { useRepoId } from '@/utils/hooks/useRepoId';
import { repoInfoStore } from '@/stores/Repo';
import { useReport } from '@/utils/hooks/useReport';
import { storeToRefs } from 'pinia';
import { openEditor } from '@/utils/editor';
const route = useRoute();
const { repoId } = useRepoId('/');
const { repoInfo, isVisitorOperate } = storeToRefs(repoInfoStore()); // pinia直接解构会失去响应式
const cloneVisible = ref(false);
const fileDownloadBaseURL = (import.meta as any).env.VITE_DOWNLOAD_HOST;
const { loginCheck } = useLoginCheck();
const showWarn = ref(false);
const downLoadWait = ref(30); // 文件频繁下载间隔
const downloadWarn = ref(`文件频繁下载,请${downLoadWait.value}s后再试`);
const handleWait = ref<any>(null);
function start() {
handleWait.value = setInterval(() => {
downLoadWait.value--;
if (downLoadWait.value <= 0) {
clearInterval(handleWait.value);
handleWait.value = null;
downLoadWait.value = 30;
}
}, 1000);
}
const reportClone = (url: String, cloneType: string, fileType?: string) => {
useReport('clone_repo_fr', {
repo_title: repoInfo.value?.name,
repo_namespace: repoInfo.value?.namespace?.name,
namespace_type: repoInfo.value?.namespace?.kind,
clone_url: url,
type: cloneType,
file_type: fileType
});
};
// const handleEnter = ()=> {
// const { id, default_branch } = repoInfo.value;
// openEditor({ project_id: String(id) , file_path: '', branch: curBranch.value || default_branch})
// }
const curBranch = computed(() => route.params.branchName);
const download = () => {
const url = `${fileDownloadBaseURL}/${repoId.value}/archive/refs/heads/${curBranch.value || repoInfo.value.default_branch}.zip`;
if (handleWait.value) {
// 阻止文件下载
downloadWarn.value = `文件频繁下载,请${downLoadWait.value}s后再试`;
showWarn.value = true;
return;
}
start();
if (loginCheck('下载源码')) {
// 上报
reportClone(url, 'download', 'zip');
window.open(url, '_self');
}
};
const handleClone = (visible: boolean) => {
cloneVisible.value = visible;
};
onUnmounted(() => {
if (handleWait.value) {
clearInterval(handleWait.value);
handleWait.value = null;
downLoadWait.value = 30;
}
});
</script>
<style lang="scss" scoped>
.clone-btn {
height: 32px;
border-radius: 4px;
border: 1px solid var(--devui-btn-common-border-color);
background: #fff;
&:hover {
background: linear-gradient(0deg, rgba(188, 188, 208, 0.1) 0%, rgba(188, 188, 208, 0.1) 100%), var(--White, #fff);
}
}
</style>

View File

@@ -0,0 +1,268 @@
<template>
<!-- 导入失败项目不展示 -->
<div v-if="repoInfo?.import_status !== 'failed'" >
<div class="flex items-center repo-header" :class="{ 'justify-end': !isPhone }">
<!-- watch -->
<d-dropdown v-if="isLogin" :position="[isPhone ? 'bottom-start' : 'bottom-end']" align="start" @toggle="noticeSettingToggle">
<div class="status-btn flex items-center px-4 cursor-pointer">
<Icon :name="noticeIconMap.get(noticeStatus) || 'gt-plane-watch'" color="#088F4E" class="mr-2" />
<span v-if="widthType !== 'sm'" class="font-color-t1 mr-2">{{
noticeMap.get(noticeStatus)
}}</span>
<span class="status-btn-number mr-2">
<Number :number="watchCount" />
</span>
<Icon name="gt-line-down" class="transition-transform" :rotate="noticeSettingStatus ? '180deg' : '0deg'" color="var(--devui-icon-fill)"></Icon>
</div>
<template #menu>
<div class="w-60 rounded-sm p-3">
<div class="pop-head flex items-center justify-between">
<div class="flex items-center"><Icon name="gt-remind" class="mr-1" /><span>通知设置</span></div>
<div>
<Icon name="gt-close" :operable="true" />
</div>
</div>
<div class="mt-2.5" @click.stop>
<d-radio-group v-model="noticeStatus" @change="updateNotice">
<div class="p-2 border border-G200 rounded-sm mb-2 w-full">
<d-radio value="default">
<div>
<p class="font-medium text-G900">仅我参与的</p>
<p class="text-xs text-CG600 mt-1">只有当我参与或被@提及时才接收来自此项目的通知</p>
</div>
</d-radio>
</div>
<div class="p-2 border border-G200 rounded-sm mb-2 w-full">
<d-radio value="all">
<div>
<p class="font-medium text-G900">全部通知</p>
<p class="text-xs text-CG600 mt-1">接收来自此项目上的所有通知</p>
</div>
</d-radio>
</div>
<div class="p-2 border border-G200 rounded-sm w-full">
<d-radio value="ignore">
<div>
<p class="font-medium text-G900">忽略通知</p>
<p class="text-xs text-CG600 mt-1">从不接收任何来自此项目的通知</p>
</div>
</d-radio>
</div>
</d-radio-group>
</div>
</div>
</template>
</d-dropdown>
<!-- star -->
<StarBtn :active="repoInfo?.starred"
:interface="starInterface(<string>repoId)"
:showText="widthType !== 'sm'"
@starChange="starResult" :number="gitcodeStarCount"></StarBtn>
<!-- fork -->
<div v-if="isForkEnable && !isRepoEmpty" class="status-btn flex items-center px-3.5 cursor-pointer">
<div class="flex items-center mr-2" @click="naviTo('repoForkCreate')">
<Icon name="gt-plane-fork" class="mr-2" />
<span v-if="widthType !== 'sm'" class="font-color-t1">Fork</span>
</div>
<span class="hover:text-link status-btn-number" @click.stop="naviTo('repoFork')">
<Number :number="gitcodeForkCount" />
</span>
</div>
</div>
<!-- github -->
<div class="font-color-t2 mt-2 flex" :class="{ 'justify-end': !isPhone }" v-if="repoInfo?.mirror_project_data?.star_count || repoInfo?.import_star_count">
<d-row style="width: fit-content" class="github-info items-center whitespace-nowrap">
<d-col flex="1" class="mr-5">
<GIcon name="gt-github1" color="var(--devui-icon-fill-weak)" class="mr-2" />
<span v-if="widthType !== 'sm'">GitHub 数据</span>
</d-col>
<d-col>
<span>
<GIcon name="gt-line-watch" class="mr-2" />
<Number :number="repoInfo?.import_watch_count || 0" />
</span>
<span class="github-info-line"></span>
<span>
<GIcon name="gt-line-star" class="mr-2" />
<Number :number="repoInfo?.mirror_project_data?.star_count || repoInfo?.import_star_count || 0" />
</span>
<span class="github-info-line"></span>
<span>
<GIcon name="gt-line-fork" class="mr-2" />
<Number :number="repoInfo?.mirror_project_data?.fork_count || repoInfo?.import_forks_count || 0" />
</span>
</d-col>
</d-row>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, nextTick, onMounted, watch } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { reqCatch } from '@/utils/catch';
import { unstarRepo, starRepo } from '@/api/repo';
import { useRepoHeaderInit } from '@/utils/hooks/useRepoHeaderInit';
import { useRepoId } from '@/utils/hooks/useRepoId';
import { useOrgId } from '@/utils/hooks/useOrgId';
import { useAccountStore } from '@/stores/user';
import { emitEvent } from '@/utils/eventBus';
import Star from '@/components/Star/index.vue';
import StarBtn from '@/components/StarBtn/StarBtn.vue';
import type { IStarOriginSource } from '@/components/StarBtn/types';
import { starInterface } from '@/components/StarBtn/starService';
const emit = defineEmits(['update-star']);
const props = withDefaults(
defineProps<{
widthType: string;
}>(),
{
widthType: 'md'
}
);
const { isLogin } = useAccountStore();
const { repoId } = useRepoId();
const { orgId } = useOrgId('/');
const { initRepoHeader, noticeStatus, updateNotice, watchCount, repoInfo } = useRepoHeaderInit(isLogin);
initRepoHeader();
// 是否允许fork
const isForkEnable = computed(() => repoInfo?.module_setting?.modules?.some(item => item.key === 'FORK' && item.value === '1'));
const isRepoEmpty = computed(()=> repoInfo.empty_repo);
const isPhone = computed(() => ['lg', 'sm'].includes(props.widthType));
const gitcodeStarCount = computed(() => {
const starCount = (repoInfo?.star_count || 0) - (repoInfo?.mirror_project_data?.star_count || 0);
return starCount >= 0 ? starCount : (repoInfo?.star_count || 0);
});
const gitcodeForkCount = computed(() => {
const forkCount = (repoInfo?.forks_count || 0) - (repoInfo?.mirror_project_data?.fork_count || 0);
return forkCount >= 0 ? forkCount : (repoInfo?.forks_count || 0);
});
const noticeMap = new Map([
['default', '仅我参与的'],
['all', 'Watch'],
['ignore', '忽略通知']
]);
const noticeSettingStatus = ref(false);
const noticeSettingToggle = (isOpen: boolean) => {
noticeSettingStatus.value = isOpen;
}
const noticeIconMap = new Map([
['default', 'gt-plane-watch-me'],
['all', 'gt-plane-watch'],
['ignore', 'gt-plane-watch-lgnore']
]);
const router = useRouter();
const route = useRoute();
const naviTo = (name: string, params?: any) => {
router.push({ name, params });
};
const handleStarList = () => {
const href = router.resolve({
name: 'repoStar',
params: {
...route.params,
namespace: orgId.value
}
});
router.push({ path: href?.href?.replace(/%2F/g, '/') });
};
const starResult = (active: boolean, count: number) => {
repoInfo.starred = active;
// star操作后总数进行计算
repoInfo.star_count = count;
emit('update-star', { star_count: repoInfo.star_count, starred: repoInfo.starred });
};
</script>
<style scoped lang="scss">
.repo-header {
white-space: nowrap;
:global(.devui-radio__material) {
flex-shrink: 0;
}
.status-btn {
height: 32px;
border-radius: 4px;
border: 1px solid var(--devui-btn-common-border-color);
background: var(--devui-btn-common-bg);
&:hover {
background: linear-gradient(0deg, rgba(188, 188, 208, 0.10) 0%, rgba(188, 188, 208, 0.10) 100%), var(--White, #FFF);
}
&-number {
padding: 0 4px;
color: var(--devui-aide-text);
}
&:not(:last-of-type) {
margin-right: 8px;
}
}
.download{
:deep(.devui-button){
&.btn-text {
padding: 0 20px;
&::after{
content:'';
position: absolute;
width: 1px;
height: 20px;
background: #fff;
right: 0;
top: 6px;
z-index: 2;
opacity: .2;
}
}
.icon-select-arrow {
color: #fff;
}
}
.icon-middle {
vertical-align: inherit
}
}
}
.github-info {
border-radius: 4px;
border: 1px solid #e3e3ee;
background: #f1f1f8;
padding: 6px 16px;
&-line {
height: 8px;
width: 1px;
background: #e3e3ee;
display: inline-block;
margin: 0 16px;
}
}
.close-icon-wrap {
.devui-icon__container {
@apply flex items-center;
}
}
:deep(.devui-radio__material-inner) {
fill: #2951E0;
}
:deep(.devui-radio:hover .devui-radio__material-inner) {
fill: #2951E0 !important;
}
</style>

View File

@@ -0,0 +1,60 @@
<template>
<div class="flex repo-info-tag items-center mt-4">
<div class="flex flex-wrap gap-2">
<div
v-for="(link, idx) in tagList"
:key="idx"
class="topic-tag flex items-center font-color-t2"
@click="triggerLink(link)"
:class="{ ['cursor-pointer']: link?.linkName || link?.linkUrl }"
:title="link?.name"
>
<Icon
v-if="/^(icon|gt)\-/.test(link?.icon)"
:name="link.icon"
color="var(--devui-shape-icon-fill)"
class="inline-block min-w-[16px]"
></Icon>
<img v-else-if="link?.icon" :src="link.icon" class="h-3.5" />
<span v-if="link?.text" class="ml-1 overflow-wrap-break whitespace-nowrap">{{ link?.text }}</span>
<span v-if="link?.suffix" class="ml-1 max-w-[160px] overflow-ellipsis overflow-hidden whitespace-nowrap">{{
link?.suffix
}}</span>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { useRouter } from 'vue-router';
const props = withDefaults(
defineProps<{
tagList: Array<object>
}>(), {
tagList: () => []
}
);
const router = useRouter();
function triggerLink(link: any) {
if (link.linkUrl) {
router.push(link.linkUrl);
} else if (link.linkName) {
router.push({ name: link.linkName, params: link.params });
}
}
</script>
<style lang="scss" scoped>
.topic-tag {
height: 24px;
border-radius: 12px;
border: 1px solid var(--devui-btn-common-border-color);
padding: 0 8px;
font-size: 12px;
&:hover {
background: $devui-disabled-bg;
color: #000 !important;
}
}
</style>

View File

@@ -0,0 +1,286 @@
<template>
<d-modal v-model="visible" :show-close="false" :draggable="false" class="repo-sync-modal min-h-[342px] rounded-md w-[90%] max-w-[700px]">
<template #header>
<d-row align="middle" class="py-3 px-4 text-[#000] bg-[#F9F9FB] rounded-md">
<d-col flex="auto" class="font-semibold">
<GIcon name="gt-plane-synchronous" class="mr-2" />同步源项目
</d-col>
<d-col class="p-2 pr-0">
<GIcon @click="handleClose" name="gt-line-close" class="cursor-pointer hover:opacity-50" />
</d-col>
</d-row>
</template>
<div class="repo-sync-modal-content">
<div class="text-[#BCBCD0]">使用同步源项目的一键操作即可获取源项目的最新代码提交和更新</div>
<div class="mt-2 gap-2 flex items-center flex-wrap">
<BranchSelect
:repo="encodeURIComponent(props.repoId)"
v-model:value="branch"
:max-height="200"
:width="150"
/>
相较于
<span class="bg-[#BCBCD0] bg-opacity-20 rounded px-2 py-1 min-w-[60px] min-h-[25px] break-all">{{ baseBrach.name_with_branch }}</span>
<span v-if="behindCount || aheadCount">滞后 {{ behindCount }} 提交超前 {{ aheadCount }} 提交</span>
<span v-else>代码一致已更新至最新版本</span>
</div>
<div v-if="loading" class="mt-8">
<d-skeleton></d-skeleton>
</div>
<div v-else>
<div class="flex mt-8 repo-sync-tab">
<div
class="repo-sync-tab-item"
@click="handleTab('behind')"
:class="{ 'repo-sync-tab-active': tabId === 'behind' }"
v-if="behindCount"
>
<span class="repo-sync-tab-text">滞后提交</span>
</div>
<div v-if="aheadCount" class="repo-sync-tab-item" @click="handleTab('ahead')" :class="{ 'repo-sync-tab-active': tabId === 'ahead' }">
<span class="repo-sync-tab-text">超前提交</span>
</div>
</div>
<div v-if="tabId === 'behind'" class="mt-4">
<div v-if="isConflict" class="bg-[#ed6d46] bg-opacity-20 rounded px-2 py-1">
代码存在冲突无法完成 Pull 更新操作
</div>
<div v-else>
<p class="text-[#BCBCD0]">对于滞后的提交你可以通过拉取更新从源项目获取最新的代码</p>
<d-popover :disabled="actionDisable !== 'inSync'" :position="['top']" content="分支同步中" trigger="hover">
<d-button color="primary" variant="solid" :disabled="actionDisable !== 'canSync'" class="mt-5 pull-button" :loading="syncLoading" @click="syncCode">拉取<span class="max-w-[150px] mx-1 ellipsis flex-1">{{ baseBrach.branch }}</span>更新</d-button>
</d-popover>
</div>
</div>
<div v-if="tabId === 'ahead'" class="mt-4">
<p class="text-[#BCBCD0]">对于超前的提交你可以通过提交 PR 的方式向源项目贡献</p>
<d-button color="primary" variant="solid" :loading="syncLoading" @click="toPullRequest" class="mt-5">创建并提交 PR</d-button>
</div>
</div>
</div>
</d-modal>
</template>
<script setup lang="ts">
import { reactive, ref, watch, onBeforeMount } from 'vue';
import BranchSelect from '@/components/BranchSelect/index.vue';
import { getBranchDiff, getIsConflict, syncBranchCode, isEnableSyncCode } from '@/api/repo';
import { useRouter } from 'vue-router';
const router = useRouter();
interface RepoInfo {
path_with_namespace: string,
forked_from_project: {
path_with_namespace: string,
path: string,
id: number
};
default_branch: string;
}
const props = defineProps<{ modelValue: boolean, repoId: string, repoInfo: RepoInfo }>();
const branch = ref(props.repoInfo?.default_branch);
const tabId = ref('behind');
const handleTab = (tab: string) => {
tabId.value = tab;
};
const visible = ref(false);
const handleOpen = () => {
visible.value = true;
getBranchDiffInfo();
};
const handleClose = () => {
handleClearLoop();
visible.value = false;
};
// 获取分支差异数
const behindCount = ref(0);
const aheadCount = ref(0);
const baseBrach = reactive({
name_with_branch: '',
path: '',
branch: ''
});
const loading = ref(false);
const actionDisable = ref('');
const getBranchDiffInfo = async() => {
// 重置分支状态
isConflict.value = false;
syncLoading.value = false;
loading.value = true;
const res = await getBranchDiff(props.repoId, branch.value);
if (!res.error) {
behindCount.value = res.data?.data?.behind || 0;
aheadCount.value = res.data?.data?.ahead || 0;
tabId.value = behindCount.value ? 'behind' : aheadCount.value ? 'ahead' : '';
baseBrach.name_with_branch = res.data?.data?.base_branch_name || '';
const path_with_branch = res.data?.data?.base_branch_path || '';
baseBrach.path = path_with_branch.split(/:(.+)/)[0];
baseBrach.branch = path_with_branch.split(/:(.+)/)[1];
if (behindCount.value) {
// 判断是否同步中
const data = await isEnableSyncCode(props.repoId, branch.value);
actionDisable.value = data.error ? 'notSync' : data.data?.data?.repo_sync_result ? 'canSync' : 'inSync';
}
}
loading.value = false;
};
// 判断合并是否存在冲突
const isConflict = ref(false);
const getConflict = async() => {
const params = {
project_id: props.repoId,
branch_name: branch.value,
other_project_id: baseBrach.path.replace(/\//g, '%2F'),
other_branch_name: baseBrach.branch
};
const res = await getIsConflict(params);
if (!res.error) {
isConflict.value = res.data?.data?.state || false;
}
};
// 分支代码同步
const syncLoading = ref(false);
const syncCode = async() => {
syncLoading.value = true;
// 判断代码是否存在冲突
await getConflict();
if (isConflict.value) {
syncLoading.value = false;
return;
}
const res = await syncBranchCode(props.repoId, branch.value);
if (!res.error) {
if (!res.data?.data?.repo_sync_result) {
handleLoop();
} else {
syncLoading.value = false;
}
} else {
syncLoading.value = false;
}
};
// 创建合并请求pr
const toPullRequest = () => {
const baseUsername = props.repoInfo?.forked_from_project?.path_with_namespace.split('/')[0];
router.push({
name: 'repoMergeCompare',
params: {
namespace: baseUsername,
repoName: props.repoInfo?.forked_from_project?.path
},
query: {
originRepo: props.repoInfo?.path_with_namespace,
targetRepo: baseBrach.path,
targetProjectId: props.repoInfo?.forked_from_project?.id,
originBranch: branch.value,
targetBranch: baseBrach.branch
}
});
handleClose();
};
// 判断是否可同步
const isEnableSync = async() => {
const res = await isEnableSyncCode(props.repoId, branch.value);
if (!res.error) {
if (res.data?.data?.repo_sync_result) {
syncLoading.value = false;
getBranchDiffInfo();
} else {
handleLoop();
}
}
};
const timer = ref(null);
// 轮询
const handleLoop = () => {
handleClearLoop();
timer.value = setTimeout(() => {
isEnableSync();
}, 2000);
};
// 清除轮询
const handleClearLoop = () => {
if (timer.value) {
clearTimeout(timer.value);
timer.value = null;
}
};
watch(() => branch.value, () => {
getBranchDiffInfo();
});
defineExpose({
handleOpen
});
onBeforeMount(() => {
handleClearLoop();
});
</script>
<style lang="scss">
@import 'devui-theme/styles-var/devui-var.scss';
.repo-sync-modal {
.devui-modal__body {
padding: 32px 16px;
color: #3B3E55;
}
svg {
color: #3B3E55!important;
}
&-content {
svg {
cursor: pointer;
&:hover {
opacity: 0.5;
}
}
.repo-sync-tab {
cursor: pointer;
&-item {
padding: 6px 8px;
}
&-text {
padding: 2px 8px;
color: #3b3e55;
}
&-active {
border-bottom: 2px solid #000;
.repo-sync-tab-text {
color: #da203e !important;
font-weight: 600;
}
}
:hover {
.repo-sync-tab-text {
border-radius: 4px;
color: #000;
background-color: rgba(#bcbcd0, 0.2);
}
}
}
.pull-button {
.button-content {
display: flex;
}
}
}
}
</style>

View File

@@ -0,0 +1,242 @@
<template>
<div class="repo-info">
<d-row :gutter="64">
<d-col flex="1" class="min-w-0">
<div class="flex items-end flex-wrap gap-x-4 gap-y-1">
<div class="text-[20px]">
<span v-for="(path, index) in breadcrumb" :key="index">
<GLink :href="path.href" >
<span class="font-color-t1 font-semibold" :class="!index ? 'max-w-[200px] whitespace-nowrap table-cell overflow-ellipsis overflow-hidden' : 'break-all'">{{ path.label }}</span>
</GLink>
<span v-if="index !== breadcrumb.length - 1" class="breadcrumb-splitter font-color-t1"> / </span>
</span>
</div>
<template v-for="(item, index) in topicData.officialTopicList" :key="index">
<div class="official-topic mb-0.5">
<GIcon name="gt-plane-officialCertification" />
<span class="ml-1">{{ item.name }}</span>
</div>
</template>
<div v-if="repoInfo.visibility === 'private'" class="repo-visible font-color-t2 inline-flex items-center justify-center text-xs">
私有
</div>
</div>
<div class="mt-4 flex items-center flex-wrap gap-x-4 gap-y-1" v-if="repoInfo?.forked_from_project">
<span class="font-color-t2 cursor-pointer" :class="{'cursor-pointer': repoInfo.value?.forked_from_project?.web_url}" @click="toForkRepo">
fork {{ repoInfo?.forked_from_project?.name_with_namespace }}
</span>
<div v-if="isAdminOperate" class="fork-btn" @click="handleSync()">
<GIcon name="gt-plane-synchronous" class="mr-1" />
同步源项目
<GIcon size="14" name="gt-line-right" class="ml-1" />
</div>
</div>
<template v-if="isPhone">
<RepoData class="mt-4" :widthType="widthType" @update-star="handleUpdateStarNumber" />
<RepoTopic :tagList="tagList" />
<RepoAction class="mt-3" />
<!-- 只在项目首页展示 -->
<DownloadChart v-if="route?.name === 'repoDashboard'" :repoId="repoId" class="mt-8" />
</template>
<template v-else>
<RepoTopic :tagList="tagList" />
</template>
</d-col>
<d-col v-if="!isPhone">
<RepoData :widthType="widthType" @update-star="handleUpdateStarNumber" />
</d-col>
</d-row>
<d-row align="middle" class="mt-32">
<d-col flex="1">
<NewNavTabs />
</d-col>
<d-col v-if="!isPhone">
<RepoAction />
</d-col>
</d-row>
<SyncFork ref="syncFork" :repoId="repoId" :repoInfo="repoInfo" />
</div>
</template>
<script lang="ts" setup>
import { ref, onUnmounted, watch, computed } from 'vue';
import RepoData from './RepoData.vue';
import RepoAction from './RepoAction.vue';
import DownloadChart from '@/components/Repo/DownloadChart.vue';
import NewNavTabs from '@/components/NavTabs/NewNavTabs.vue';
import RepoTopic from './RepoTopic.vue';
import SyncFork from './SyncFork.vue';
import { addEventListener, offEvent } from '@/utils/eventBus';
import { reqCatch } from '@/utils/catch';
import { storeToRefs } from 'pinia';
import { useRouter, useRoute } from 'vue-router';
import * as types from '@/api/org/types';
import { getCommunityInfo } from '@/api/org/index';
import { getOrgInfo } from '@/views/Org/hooks/orgInfo';
import { orgInfoStore } from '@/stores/Org/index';
import { useRepoHeaderInit } from '@/utils/hooks/useRepoHeaderInit';
import { useRepoId } from '@/utils/hooks/useRepoId';
import { useAccountStore } from '@/stores/user';
import { getSettingsValues } from '@/api/repo';
import { repoInfoStore } from '@/stores/Repo';
import { useOrgId } from '@/utils/hooks/useOrgId';
import setting from '@/setting';
const props = withDefaults(
defineProps<{
widthType: string;
}>(),
{
widthType: 'md'
}
);
const { isLogin } = useAccountStore();
const { repoId } = useRepoId();
const { orgId } = useOrgId('/');
const route = useRoute();
const { access_level, repoInfo, isAdminOperate } = storeToRefs(repoInfoStore()); // pinia直接解构会失去响应式
const { setRepoInfo } = repoInfoStore();
// 获取组织namespace
const { namespace } = getOrgInfo();
const { initRepoDashboard, validLinks, topicData } = useRepoHeaderInit(isLogin);
initRepoDashboard();
const tagList = ref(validLinks.value);
const isPhone = computed(() => ['lg', 'sm'].includes(props.widthType));
const visiMap = new Map([
['public', '公开'],
['private', '私有'],
['internal', '内部']
]);
const toForkRepo = () => {
if (repoInfo.value?.forked_from_project?.web_url) {
location.href = repoInfo.value?.forked_from_project?.web_url;
}
};
const router = useRouter();
const breadcrumb = computed(() => {
const pathStrList = repoInfo.value.name_with_namespace?.split('/');
const pathList = pathStrList.map((item) => ({
label: item
}));
// 第一个路由带链接
if (pathList.length === 2) {
pathList[0].href = router.resolve({
name: 'homepage',
params: { namespace: orgId.value }
}).path;
pathList[1].href = router.resolve({
name: 'repoDashboard'
}).path;
}
return pathList;
});
// 获取仓库
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);
}
};
async function getModuleState() {
const res = await getSettingsValues({
repoId: repoId.value,
group_id: namespace.value
});
if (res.data) {
const { modules } = res.data.data;
modules.forEach(({ key, value }) => {
if (key === 'COMMUNITY' && value === '1') {
getCommunityInfoFun();
}
});
}
}
const handleUpdateStarNumber = (params:Record<string, any>) => {
setRepoInfo(params);
};
const syncFork = ref(null);
const handleSync = () => {
syncFork.value.handleOpen();
};
// @TODO 此处只用于repo切换时读取项目模块配置信息。是否直接从repo详情获取
watch(repoId, () => getModuleState(), { flush: 'post', immediate: true });
addEventListener('updateNaviBar', getModuleState);
onUnmounted(() => offEvent('updateNaviBar', getModuleState));
watch(() => topicData.userTopicList, val => {
if (val && val.length) {
tagList.value = validLinks.value.concat(topicData.userTopicList);
}
});
watch(() => validLinks.value, val => {
if (val && val.length) {
tagList.value = validLinks.value.concat(topicData.userTopicList);
}
}, { deep: true });
</script>
<style scoped lang="scss">
.repo-info {
.repo-visible {
border-radius: 14px;
border: 1px solid var(--devui-btn-common-border-color);
padding: 7px;
line-height: 16px;
height: 24px;
}
.official-topic {
background: rgba(#2951E0, 0.12);
border-radius: 4px;
padding: 0 8px;
height: 22px;
color: #2951E0;
font-weight: 600;
}
.status-btn-tag {
height: 24px;
border-radius: 12px;
border: 1px solid #e3e3ee;
padding: 0 8px;
font-size: 12px;
&:hover {
background: #f1f1f8;
color: #000 !important;
}
&:not(:last-of-type) {
margin-right: 4px;
}
}
.fork-btn {
border-radius: 4px;
border: 1px solid #E3E3EE;
background: #fff;
padding: 2px 8px;
cursor: pointer;
display: flex;
align-items: center;
&:hover {
background: linear-gradient(0deg, rgba(188, 188, 208, 0.1) 0%, rgba(188, 188, 208, 0.1) 100%), var(--White, #fff);
}
}
}
</style>

View File

@@ -0,0 +1,86 @@
<script setup lang="ts">
import { useRouter } from 'vue-router';
const router = useRouter();
defineProps<{
repoId: string;
repoInfo: any;
validLinks: any[];
topicList: any[];
loadingStatus: {
profileLoading: boolean;
readmeLoading: boolean;
eventsLoading: boolean;
contributorLoading: boolean;
releasesLoading: boolean;
};
editable: boolean;
}>();
function triggerLink(link: any) {
if (link.linkName) router.push({ name: link.linkName, params: link.params });
}
const handleClickTopic = (name: string) => {
if (name) {
router.push(`/topic/${name}`);
} else {
router.push('/404');
}
};
</script>
<template>
<div class="repo-info">
<d-skeleton :loading="loadingStatus.profileLoading" :rows="10">
<div class="repo-profile">
<div class="profile-title flex items-center justify-between">
<div class="flex items-center">
<Icon name="gt-file2-c" class="mr-2"></Icon><span class="text-G900 font-medium">简介</span>
</div>
<div v-if="editable" @click="triggerLink({linkName:'repoSetting'})" class="cursor-pointer" title="编辑简介">
<Icon name="gt-edit"></Icon>
</div>
</div>
<div class="text-G900 break-words mt-3">
<g-text :config="{ line: 4, ellipse: true,tooltip:false, showAll: true }">
{{ repoInfo.description || '暂无简介' }}
</g-text>
</div>
<!-- <div class="my-4 flex flex-wrap w-[100%]" v-if="topicList?.length">
<div v-for="topic of topicList" :key="topic.name" class="border-box max-w-[100%] truncate py-[2px] px-2 border-solid border-[1px] border-CG300 rounded-[3px] text-CG600 text-[12px] mr-1 mb-1 cursor-pointer font-bold hover:text-CG900" @click.stop="handleClickTopic(topic.name)">
<d-popover :content="topic.name" trigger="hover">
{{ topic.name }}
</d-popover>
</div>
</div> -->
<div class="mt-6">
<div
v-for="(link, idx) in validLinks"
:key="idx"
class="text-G900 mb-3 flex items-center"
style="line-height: 1"
@click="triggerLink(link)"
:class="{ ['cursor-pointer']: link.linkName }"
:title="link.name"
>
<Icon v-if="/^(icon|gt)\-/.test(link.icon)" :name="link.icon" size="16px" class="inline-block min-w-[16px]"></Icon>
<img v-else :src="link.icon" class="h-3.5" />
<span
class="ml-2 overflow-wrap-break"
style="word-break: break-all; cursor: pointer"
>{{ link.text }}</span>
<span class="ml-0.5" v-if="link.suffix">{{ link.suffix }}</span>
</div>
</div>
<div class="g-row-line my-24"></div>
</div>
</d-skeleton>
</div>
</template>
<style scoped lang="scss">
@import 'devui-theme/styles-var/devui-var.scss';
</style>

View File

@@ -0,0 +1,115 @@
<script setup lang="ts">
import { usePageResize } from '@/utils/hooks/usePageResize';
import { ButtonGroup } from 'vue-devui/button';
defineProps({
starred: {
default: false
},
starCount: {
default: 0
},
disabled: {
default: false
}
});
defineEmits<{(event: 'star'): void; (event: 'gotoStarList'): void }>();
const { isMobile } = usePageResize();
</script>
<template>
<div class="star-status">
<!-- 当前 + 计数 -->
<ButtonGroup v-if="!isMobile" size="sm">
<d-button
class="star-tag border-G400"
:class="{ starred: starred }"
:disabled="disabled"
@click="$emit('star')"
>
<Icon
:name="starred ? 'gt-starred-c' : 'gt-star'"
:color="starred ? '#f9a026' : undefined"
size="13"
/>
<span class="star-tag-text">{{ starred ? 'Starred' : 'Star' }}</span>
</d-button>
<d-button class="star-number border-G400" @click="$emit('gotoStarList')">
<Number :number="starCount" />
</d-button>
</ButtonGroup>
<!-- 当前 -->
<d-button
v-if="isMobile"
class="star-tag border-G400"
:class="{ starred: starred }"
:disabled="disabled"
@click="$emit('star')"
>
<Icon
:name="starred ? 'gt-starred-c' : 'gt-star'"
:color="starred ? '#f9a026' : undefined"
size="13"
/>
<span class="star-tag-text">{{ starred ? 'Starred' : 'Star' }}</span>
</d-button>
</div>
</template>
<style scoped lang="scss">
@import 'devui-theme/styles-var/devui-var.scss';
.star-tag {
height: 24px;
font-size: $devui-font-size-sm;
display: inline-flex;
align-items: center;
padding: 0 8px;
gap: 4px;
:deep(.button-content) {
display: inline-flex;
gap: 4px;
align-items: center;
i {
display: inline-block;
width: 12px;
height: 12px;
}
}
}
.star-number {
height: 24px;
font-size: $devui-font-size-sm;
background: #f8f9fb;
}
.star-tag,
.star-number {
border: 1px solid #d1d2d4;
&:focus,
&:hover {
color: unset;
background-color: unset;
border-color: #d1d2d4;
}
}
.star-status {
:deep(.devui-button--sm) {
min-width: auto;
}
}
@media screen and (max-width: 1000px) {
.star-tag {
height: 28px !important;
line-height: 28px !important;
}
}
</style>

View File

@@ -0,0 +1,29 @@
<!-- 项目事件 -->
<script setup lang="ts">
import { rdEvent } from '@/utils/hooks/useRepoInit';
import { useTimeFormat } from '@/utils/hooks/useTimeFormat';
import EventAdapter from '@/components/EventAdapter/index.vue';
interface Event {
eventData: any;
fullPath?: string;
namespace?: any;
}
defineProps<Event>();
const { formatTimeFromNow } = useTimeFormat();
</script>
<template>
<div>
<div class="desc flex items-center g-max-width gap-2">
<a class="flex items-center" :href="eventData.author?.web_url">
<GAvatar :name="eventData.author_username" :src="eventData.author.avatar_url" :width="16" :height="16"></GAvatar>
</a>
<div class="px-1 flex-shrink-0 text-CG600"><span>{{eventData.author_username}}</span></div>
<div><span>{{ rdEvent(eventData) }}</span></div>
<div class="text-CG500 px-1"><span>{{ formatTimeFromNow(eventData.created_at) }}</span></div>
</div>
<div class="detail px-6 mt-1">
<EventAdapter :event-data="eventData"/>
</div>
</div>
</template>