搜索结果列表页面开发
This commit is contained in:
56
src/components/Repo/ActivityCard.vue
Normal file
56
src/components/Repo/ActivityCard.vue
Normal 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>
|
||||
72
src/components/Repo/ContributeCard.vue
Normal file
72
src/components/Repo/ContributeCard.vue
Normal 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>
|
||||
122
src/components/Repo/DownloadChart.vue
Normal file
122
src/components/Repo/DownloadChart.vue
Normal 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>
|
||||
33
src/components/Repo/ForkButton.vue
Normal file
33
src/components/Repo/ForkButton.vue
Normal 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>
|
||||
113
src/components/Repo/MarkdownCard.vue
Normal file
113
src/components/Repo/MarkdownCard.vue
Normal 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>
|
||||
106
src/components/Repo/NoticeStatus.vue
Normal file
106
src/components/Repo/NoticeStatus.vue
Normal 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>
|
||||
62
src/components/Repo/ReleaseCard.vue
Normal file
62
src/components/Repo/ReleaseCard.vue
Normal 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">
|
||||
发布于 {{ formatTimeFromNow(rls.created_at) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</DataPanel>
|
||||
</div>
|
||||
</template>
|
||||
209
src/components/Repo/RepoHeaderInfo/CloneRepo.vue
Normal file
209
src/components/Repo/RepoHeaderInfo/CloneRepo.vue
Normal 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>
|
||||
110
src/components/Repo/RepoHeaderInfo/RepoAction.vue
Normal file
110
src/components/Repo/RepoHeaderInfo/RepoAction.vue
Normal 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>
|
||||
268
src/components/Repo/RepoHeaderInfo/RepoData.vue
Normal file
268
src/components/Repo/RepoHeaderInfo/RepoData.vue
Normal 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>
|
||||
60
src/components/Repo/RepoHeaderInfo/RepoTopic.vue
Normal file
60
src/components/Repo/RepoHeaderInfo/RepoTopic.vue
Normal 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>
|
||||
286
src/components/Repo/RepoHeaderInfo/SyncFork.vue
Normal file
286
src/components/Repo/RepoHeaderInfo/SyncFork.vue
Normal 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>
|
||||
242
src/components/Repo/RepoHeaderInfo/index.vue
Normal file
242
src/components/Repo/RepoHeaderInfo/index.vue
Normal 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>
|
||||
86
src/components/Repo/RepoIntro.vue
Normal file
86
src/components/Repo/RepoIntro.vue
Normal 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>
|
||||
115
src/components/Repo/StarStatus.vue
Normal file
115
src/components/Repo/StarStatus.vue
Normal 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>
|
||||
29
src/components/Repo/eventAdapter.vue
Normal file
29
src/components/Repo/eventAdapter.vue
Normal 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>
|
||||
Reference in New Issue
Block a user