搜索结果列表页面开发

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,178 @@
<template>
<user-info-card
class="user-info"
v-bind="userInfoData"
:hideFollowData="hideFollowData"
:profileLoading="profileLoading"
:isPrivate="isPrivate"
>
<template v-if="!userInfoData.isSelf" #action>
<div class="user-info-actions">
<d-button :loading="loading" class="user-info-button" @click="onToggleStar">
<Icon
v-if="!loading"
:name="userInfoData.hasFollowed ? 'gt-starred-c' : 'gt-star'"
:color="userInfoData.hasFollowed ? 'var(--color-Y500)' : ''"
size="16px"
/>
<span class="ml-2">
{{ userInfoData.hasFollowed ? '已关注' : '关注' }}
</span>
</d-button>
</div>
</template>
</user-info-card>
</template>
<script lang="ts">
export default {
name: 'user-info'
};
</script>
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue';
import { useUserInfo } from '@/views/User/hooks/useUserInfo';
import UserInfoCard from '@/components/UserInfoCard/index.vue';
import { getUserProfile } from '@/api/user';
import { reqCatch } from '@/utils/catch';
import { otherAccountStore } from '@/stores/user';
import type { UserProfile } from '@/api/user/types';
import type { AxiosResponse } from 'axios';
import { useStarFollow } from '@/views/User/hooks/useStarFollow';
import { useRouter } from 'vue-router';
import { xssPurify } from '@/utils';
import { useIsPrivate } from '@/views/User/hooks/useIsPrivate';
const { isPrivate } = useIsPrivate();
const router = useRouter();
// 状态仓库
const otherStore = otherAccountStore();
const emits = defineEmits<{(e: 'updateBanner', image: string): void; (e: 'updateInfo', data: any): void }>();
const profileLoading = ref(true);
// 用户信息
const userInfoData = ref({
name: '',
namespace: '',
avatarUrl: '',
description: '',
fansCount: 0,
followCount: 0,
tenant: '',
location: '',
email: '',
github: '',
blog: '',
username: '',
profile: {},
isFollow: false,
isSelf: false,
hasFollowed: false
});
const { isSelf, userInfo, namespace } = useUserInfo();
const { followCount, fanCount, hasFollowed, loading, hideFollowData, toggleStar, getAllCounts, checkFollowedUser } =
useStarFollow(
{
namespace,
username: userInfo.username
},
false
);
checkFollowedUser();
watch(() => [fanCount.value, followCount.value], () => {
userInfoData.value.fansCount = fanCount.value;
userInfoData.value.followCount = followCount.value;
userInfoData.value.hasFollowed = hasFollowed.value;
});
watch(
() => hasFollowed.value,
(val, oldVal) => {
getAllCounts();
}
);
watch(
() => otherStore.accountInfo.isFollow,
(val) => {
hasFollowed.value = val || false;
otherStore.saveFollowed(val);
}
);
const onToggleStar = () => {
if (loading.value) return false;
toggleStar(userInfo.username || '', namespace, !hasFollowed.value);
};
onMounted(async() => {
let userData = userInfo;
if (!isSelf) userData = {};
const username = namespace;
const res = await reqCatch(getUserProfile, { username });
if (!res.data) {
// 审核未通过跳404页
router.replace('/404');
}
const infoData = (res.data as AxiosResponse<UserProfile>).data || { avatar: '', profile: {}};
const $name = infoData.nickname || '';
userInfoData.value = {
name: $name ? xssPurify($name) : '',
namespace: infoData.username ? `@${infoData.username}` : `@${namespace}`,
avatarUrl: infoData.avatar || '',
description: infoData.profile.description || '',
fansCount: infoData.fans || 0,
followCount: infoData.concerns || 0,
tenant: infoData.profile.company || '',
location: infoData.profile.location || '',
email: !infoData.profile.email_private ? infoData.profile.show_email : '',
github: infoData.profile.github_account.replace(/(https?:\/\/)?github\.com\//, ''),
blog: infoData.profile.website || '',
username: infoData.username || namespace,
profile: infoData.profile,
isFollow: hasFollowed.value,
isSelf
};
profileLoading.value = false;
sessionStorage.setItem('curName', userInfoData.value.name);
emits('updateBanner', infoData.profile.bg_image);
emits('updateInfo', infoData);
otherStore.saveAccountInfo(userInfoData.value);
if (isSelf || !infoData?.profile?.setting_private) getAllCounts();
});
defineExpose({
getAllCounts
});
</script>
<style scoped lang="scss">
.user-info {
&-actions {
margin: 24px 0;
width: 100%;
}
&-button {
display: block;
width: 180px;
padding: 8px 16px;
line-height: 1 !important;
background-color: white;
border-color: var(--devui-line, #d7d8da);
color: var(--color-text);
:deep(.button-content) {
display: inline-flex;
align-items: center;
line-height: 1;
}
&:active,
&:hover {
color: var(--color-text) !important;
border-color: var(--devui-line, #d7d8da);
}
}
}
</style>

View File

@@ -0,0 +1,524 @@
<template>
<panel class="activity-contributes overflow-hidden">
<template #header>
<div v-if="!mobile && !hideContributes" class="activity-contributes-activities-header">
<d-dropdown class="activity-contributes-activities-select" @toggle="onToggle">
<d-button class="activity-contributes-activities-trigger">
<div class="activity-contributes-activities-trigger-inner">
<span class="activity-contributes-activities-trigger-text">{{ currentYear }}</span>
<d-icon :class="{ 'is-open': isOpen }" name="select-arrow" size="16px"></d-icon>
</div>
</d-button>
<template #menu>
<div class="activity-contributes-activities-item" v-for="item in years" @click="handleChange(item)" :key="item.value">
{{ item.label }}
</div>
</template>
</d-dropdown>
<div class="activity-contributes-activities-other">
<span class="activity-contributes-activities-other-text">贡献了{{ count }}</span>
<d-tooltip content="统计数据包括Issue、代码提交、Pull Request等">
<Icon name="gt-tooltip" size="16px" color="#707A87" />
</d-tooltip>
</div>
</div>
<div v-else class="activity-contributes-activities-header mobile-header">
<Icon name="gt-file-code-c" size="16px" style="margin-right: 10px" />
<span>贡献</span>
</div>
</template>
<div class="relative">
<div class="activity-contributes-activities-contribute">
<DChart v-if="!refresh" :option="option" style="width: 100%; height: 132px" @chartReady="onReady"></DChart>
</div>
<div class="activity-contributes-activities-content" :class="{ 'is-mobile': mobile }">
<d-action-timeline :data="activitiesData" :load-more-config="loadMoreConfig" @action-load-more="loadMore(currentYear, currentDay)">
<template #content="{ option }">
<div class="activity-contributes-activities-timeline-action" @click="option.open = !option.open">
<g-text>
<span class="activity-contributes-activities-timeline-title">
<!--项目相关-->
<template v-if="option.target === 'project'">
{{ option.action }}{{ option.targetType }}
<a :href="option.projectLink" target="_blank" class="font-bold underline">
{{ option.project }}
</a>
</template>
<template v-else-if="['deleted'].includes(option.enAction) && option.pushData">
在项目<a :href="option.projectLink" target="_blank" class="font-bold underline">
{{ option.project }} </a
> {{ option.action }}了一个{{ option.targetType }}
</template>
<template
v-else-if="
['joined', 'created', 'deleted', 'imported', 'opened', 'left', 'follow'].includes(option.enAction)
"
>
<template v-if="!option.project">
{{ option.action }}了组织
<a :href="option.groupLink" target="_blank" class="font-bold underline">
{{ option.groupData.name }}
</a>
</template>
<template v-else>
{{ option.action }}了项目
<a :href="option.projectLink" target="_blank" class="font-bold underline">
{{ option.project }}
</a>
</template>
</template>
<template v-else-if="option.enAction === 'commented on'">
在项目
<a :href="option.projectLink" target="_blank" class="font-bold underline"> {{ option.project }} </a
>{{ option.action }}
</template>
<template v-else-if="['pushed new', 'pushed to'].includes(option.enAction)">
在项目
<a :href="option.projectLink" target="_blank" class="font-bold underline"> {{ option.project }} </a
>{{ option.action }}{{ option.targetType }}
</template>
<template
v-else-if="
['created', 'imported', 'destroyed', 'changed'].includes(option.enAction) &&
option.target === 'label'
"
>
在项目
<a :href="option.projectLink" target="_blank" class="font-bold underline">
{{ option.project }}
</a>
里{{ option.action }}了一个{{ option.targetType }}
</template>
<template
v-else-if="
['created', 'destroyed', 'changed'].includes(option.enAction) && option.target === 'group'
"
>
{{ option.action }}了一个{{ option.targetType }}
</template>
<template
v-else-if="
['opened', 'closed', 'accepted', 'created'].includes(option.enAction) &&
option.target === 'merge request'
"
>
在项目
<a :href="option.projectLink" target="_blank" class="font-bold underline">
{{ option.project }}
</a>
里{{ option.action }}了一个{{ option.targetType }}
</template>
<template
v-else-if="
['opened', 'closed', 'created', 'destroyed'].includes(option.enAction) &&
option.target === 'milestone'
"
>
在项目
<a :href="option.projectLink" target="_blank" class="font-bold underline">
{{ option.project }}
</a>
里{{ option.action }}了一个{{ option.targetType }}
</template>
<template
v-else-if="['opened', 'closed', 'created'].includes(option.enAction) && option.target === 'issue'"
>
在项目
<a :href="option.projectLink" target="_blank" class="font-bold underline">
{{ option.project }}
</a>
里{{ option.action }}了一个{{ option.targetType }}
</template>
</span>
</g-text>
<d-icon :name="option.open ? 'chevron-up' : 'chevron-down'"></d-icon>
</div>
<div v-show="option.open" class="activity-contributes-activities-timeline-content">
<template v-if="option.target === 'project'">
<div v-for="(item, key) in option.actionContent" :key="key" class="mt-3">
<template v-if="String(key) === 'visibility_level'">
<span class="font-bold">项目权限:</span>
<template v-if="!Array.isArray(item)">"{{ item }}"</template>
<template v-else>"{{ getRepoAuthLevel(item[0] || 0) }}" {{ option.action }}为 "{{ getRepoAuthLevel(item[1] || 0) }}"</template>
</template>
<template v-else-if="String(key) === 'name'">
<span class="font-bold mr-1">名称:</span>
<template v-if="!Array.isArray(item)">"{{ item }}"</template>
<template v-else>"{{ item[0] || '' }}" {{ option.action }}为 "{{ item[1] || '' }}"</template>
</template>
<template v-else-if="String(key) === 'description'">
<span class="font-bold">描述:</span>
<template v-if="!Array.isArray(item)">"{{ item }}"</template>
<template v-else>"{{ item[0] || '' }}" {{ option.action }}为 "{{ item[1] || '' }}"</template>
</template>
<template v-else-if="Array.isArray(item)">
<span class="font-bold">{{ PROP_TYPES[key] || key }}:</span> "{{ item[0] ?? '' }}"
{{ option.action }}为 "{{ item[1] ?? '' }}"
</template>
<template v-else>
<span class="font-bold">{{ PROP_TYPES[key] || key }}:</span> "{{ item }}"
</template>
</div>
</template>
<template v-else-if="['deleted'].includes(option.enAction) && option.pushData">
在项目<a :href="option.projectLink" target="_blank" class="font-bold underline">
{{ option.project }} </a
>里 {{ option.action }}了一个{{ option.targetType }}: {{ option.pushData?.ref }}
</template>
<template
v-else-if="
['joined', 'created', 'deleted', 'imported', 'opened', 'left'].includes(option.enAction) &&
!option.target
"
>
<template v-if="!option.project"> {{ option.action }}了一个组织 </template>
<template v-else> {{ option.action }}了一个项目 </template>
</template>
<template v-else-if="option.enAction === 'commented on'">
在项目<a :href="option.projectLink" target="_blank" class="font-bold underline">
{{ option.project }} </a
>{{ option.action }}:
<a class="font-bold underline" :href="option.actionLink" target="_blank">{{ option.targetTitle }}</a>
</template>
<template v-else-if="['pushed new', 'pushed to'].includes(option.enAction)">
在<a :href="option.projectLink" target="_blank" class="font-bold underline"> {{ option.project }} </a
>{{ option.action }}{{ option.targetType }}:
<a class="font-bold underline" :href="option.projectLink" target="_blank">{{
option.pushData?.ref_type === 'branch'
? option.pushData?.ref
: option.pushData?.commit_title || option.pushData?.ref
}}</a>
</template>
<template
v-else-if="
['created', 'imported', 'destroyed', 'changed'].includes(option.enAction) && option.target === 'label'
"
>
<span>{{ option.action }}一个{{ option.targetType }}</span
>: {{ option.targetTitle }}
</template>
<template
v-else-if="['created', 'destroyed', 'changed'].includes(option.enAction) && option.target === 'group'"
>
<span>{{ option.action }}一个{{ option.targetType }}</span
>: {{ option.targetTitle }}
</template>
<template
v-else-if="
['opened', 'closed', 'accepted', 'created'].includes(option.enAction) &&
option.target === 'merge request'
"
>
<span>{{ option.action }}一个{{ option.targetType }}</span>
<a :href="option.actionLink" target="_blank" class="font-bold underline">{{ option.targetTitle }}</a
> {{ option.mergeInfo?.source_branch }}合并到{{ option.mergeInfo.target_branch }}
</template>
<template
v-else-if="
['opened', 'closed', 'created', 'destroyed'].includes(option.enAction) &&
option.target === 'milestone'
"
>
{{ option.action }}了一个{{ option.targetType
}}<a :href="option.actionLink" target="_blank" class="font-bold underline">{{ option.targetTitle }}</a>
</template>
<template
v-else-if="['opened', 'closed', 'created'].includes(option.enAction) && option.target === 'issue'"
>
在项目
<a :href="option.projectLink" target="_blank" class="font-bold underline">
{{ option.project }}
</a>
里{{ option.action }}了一个{{ option.targetType }}:
<a class="font-bold underline" :href="option.actionLink" target="_blank">{{ option.targetTitle }}</a>
</template>
</div>
</template>
</d-action-timeline>
</div>
<div v-if="hideContributes" class="activity-contributes-layer">
<span class="activity-contributes-layer-text">
<span class="max-w-[200px] leading-[35px] truncate inline-block">{{ user?.accountInfo?.name }}</span
><span class="mt-[7px]">隐藏了 Ta 的个人动态</span>
</span>
</div>
</div>
</panel>
</template>
<script lang="ts">
export default {
name: 'activity-contributes'
};
</script>
<script setup lang="ts">
import { ref, reactive, watch, nextTick } from 'vue';
import Panel from '@/components/Panel/index.vue';
import { useContributes } from '@/views/User/hooks/useContributes';
import { useTimelineActivities } from '@/views/User/hooks/useTimelineActivities';
import { PROP_TYPES } from '@/views/User/constant/const';
import { otherAccountStore } from '@/stores/user';
import { useUserInfo } from '@/views/User/hooks/useUserInfo';
import { DChart } from 'vue-devui/echarts';
const props = withDefaults(
defineProps<{
mobile?: boolean;
}>(),
{
mobile: false
}
);
const emits = defineEmits<{(e: 'hideMessage'): void }>();
const repoAuthDict: Record<string, any> = {
'0': '私有',
'20': '公开'
};
const getRepoAuthLevel = (level: number | string) => {
const l = level.toString();
return repoAuthDict[l] || l;
};
// 贡献和动态
const getYear: string = `${new Date().getFullYear()}`;
const getYears = [];
for (let i = 2023; i <= Number(getYear); i++) {
getYears.push({
label: `${i}`,
value: `${i}`
});
}
const currentYear = ref<string>(getYear);
const currentDay = ref<string>('');
const years = ref(getYears);
const isOpen = ref<boolean>(false);
const onToggle = (val: boolean) => {
isOpen.value = val;
};
const { isSelf } = useUserInfo();
const user = otherAccountStore();
const { option, refresh, hideContributes, count, getContributesData } = useContributes(currentYear.value, props.mobile);
const { getData, activitiesData, loadMore, getDayData, isEnd, allData, nextPage } = useTimelineActivities(currentYear.value);
watch(
() => isSelf + user?.accountInfo?.profile?.setting_private,
() => {
if (isSelf) {
getContributesData(currentYear.value);
getData(currentYear.value);
} else {
if (user?.accountInfo?.profile) {
if (user.accountInfo?.profile?.setting_private) {
hideContributes.value = true;
} else {
getContributesData(currentYear.value);
getData(currentYear.value);
}
}
}
},
{
immediate: !!user?.accountInfo?.profile
}
);
const loadMoreConfig = reactive({
loadMore: true,
loadMoreText: '加载更多'
});
watch(
() => isEnd.value,
(val) => {
if (val) {
loadMoreConfig.loadMore = false;
return false;
}
loadMoreConfig.loadMore = true;
},
{ flush: 'post' }
);
const onReady = (echarts: any) => {
echarts.on('click', (params: any) => {
const val = params.value || {};
const day = val[0];
allData.value = {};
if (currentDay.value === day) {
nextPage.value = '';
currentDay.value = '';
} else {
// hack一下,接口暂不支持查询具体某一天的事件, 手动限制事件并且过滤返回值
const dateObject = new Date(`${day}T23:59:59.999Z`);
// 获取 ISO 8601 格式的字符串
nextPage.value = dateObject.toISOString();
currentDay.value = day;
}
getDayData(currentYear.value, currentDay.value);
});
};
watch(
() => hideContributes.value,
(val) => {
if (val) {
emits('hideMessage');
}
}
);
const handleChange = (item: any) => {
// 不校验是否改变,用于初始化查看全部事件
currentYear.value = item.value;
nextPage.value = '';
currentDay.value = '';
allData.value = {};
isEnd.value = false;
nextTick(() => {
getContributesData(item.value);
getData(item.value);
});
};
</script>
<style scoped lang="scss">
$activity-contributes-border-color: #808080;
$activity-contributes-border-color-second: #e6e6e8;
$activity-contributes-border-radius: 4px;
$activity-contributes-color: var(--color-G900);
$activity-contributes-color-second: #2d2d2e;
$activity-contributes-color-third: var(--color-CG600);
.activity-contributes {
:deep(.g-panel-header) {
padding: 4px 0;
background: #ffffff;
}
&-activities {
:deep(.g-panel-header) {
padding: 0;
}
&-header {
&.mobile-header {
display: flex;
align-items: center;
padding: 0 22px;
height: 36px;
}
}
&-select {
}
&-item {
width: 110px;
height: 40px;
line-height: 40px;
padding: 0 26px;
font-size: 16px;
font-weight: 500;
color: $activity-contributes-color;
&:hover {
cursor: pointer;
}
}
&-trigger {
background-color: var(--color-CG100);
margin: -4px 0;
border: none;
height: 40px !important;
width: 113px;
border-radius: 0;
&-inner {
display: flex;
align-items: center;
gap: 16px;
padding-left: 6px;
}
&-text {
font-size: 16px;
font-weight: 500;
color: $activity-contributes-color;
margin-bottom: -2px;
}
}
.is-open {
transform: rotate(180deg);
}
&-other {
display: inline-flex;
align-items: center;
padding-left: 20px;
color: $activity-contributes-color-third;
line-height: 1;
&-text {
font-size: 14px;
padding-right: 8px;
}
}
&-content {
margin-top: 8px;
:deep(.border-bottom) {
display: none;
}
:deep(.icon) {
overflow: inherit;
}
:deep(.dp-action-timeline-operation) {
border: none;
color: $activity-contributes-color-third;
}
&.is-mobile {
:deep(.dp-action-timeline) {
padding: 0;
}
}
}
&-timeline {
&-action {
display: flex;
align-items: center;
justify-content: space-between;
background: linear-gradient(180deg, #fcfcfc 0%, #f8f9fb 100%);
border-radius: $activity-contributes-border-radius;
height: 32px;
line-height: 32px;
padding: 0 8px;
margin-top: -6px;
&:hover {
cursor: pointer;
}
}
&-title {
color: $activity-contributes-color;
font-size: 14px;
}
&-content {
padding: 8px 16px;
}
}
}
&-layer {
position: absolute;
width: calc(100% + 16px);
height: calc(100% + 16px);
top: -8px;
left: -8px;
background-color: rgba(217, 217, 217, 0.1);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
&-text {
display: flex;
font-size: 24px;
font-weight: 500;
line-height: 1;
color: $activity-contributes-color;
}
}
}
</style>

View File

@@ -0,0 +1,175 @@
<template>
<Icon class="banner-choose-trigger" name="gt-upload-cover" size="16px" color="#7E7E80" @click="onChange" />
<d-modal class="banner-choose" v-model="visible" :title="title" :style="{ width: mobile ? '320px' : '600px' }">
<div class="banner-choose-contain">
<div class="banner-choose-content">
<div class="banner-choose-row"
v-for="(item, index) in bannerList" :key="index"
:class="{ 'is-selected': selectBanner?.title === item.title }"
@click="onChoose(item)"
>
<div class="banner-choose-row-title">{{ item.title }}</div>
<div class="banner-choose-row-img">
<img :src="`${item.img}`" :title="item.title" :alt="item.title" />
</div>
</div>
</div>
</div>
<div class="g-modal-footer">
<d-button class="banner-choose-button" color="secondary" :loading="loading" @click="onCancel">取消</d-button>
<d-button class="banner-choose-button" variant="solid" :loading="loading" :disabled="!selectBanner"
@click="onConfirm">确认</d-button>
</div>
</d-modal>
</template>
<script lang="ts">
export default {
name: 'banner-choose'
};
</script>
<script setup lang="ts">
import { ref } from 'vue';
withDefaults(defineProps<{
title: string;
mobile?: boolean;
}>(), {
title: 'Banner设置',
mobile: false
});
const emits = defineEmits<{(e: 'update', data: { title: string; img: string; }): void;
(e: 'change'): void;
}>();
const visible = ref(false);
const selectBanner = ref<{ title: string; img: string; }>(null);
const loading = ref(false);
const bannerList = ref<{ title: string; img: string; }[]>([]);
const onChoose = (item: { title: string; img: string; }) => {
selectBanner.value = item;
};
const getBannerList = () => {
bannerList.value = [
{
'title': '白鲸',
'img': 'https://cdn-img.gitcode.com/user-profile/skin-blackwhale.png'
},
{
'title': '未来建筑',
'img': 'https://cdn-img.gitcode.com/user-profile/skin-building.jpg'
},
{
'title': '橘猫',
'img': 'https://cdn-img.gitcode.com/user-profile/skin-cat-title.png'
},
{
'title': '缤纷色彩',
'img': 'https://cdn-img.gitcode.com/user-profile/skin-colorful.jpg'
},
{
'title': '编程人生',
'img': 'https://cdn-img.gitcode.com/user-profile/skin-growway.png'
},
{
'title': '20 周年',
'img': 'https://cdn-img.gitcode.com/user-profile/skin-number.png'
},
{
'title': '猿力爆发',
'img': 'https://cdn-img.gitcode.com/user-profile/skin-thespecialists.gif'
},
{
'title': '波浪',
'img': 'https://cdn-img.gitcode.com/user-profile/skin-wave.jpg'
}
];
};
const onChange = () => {
visible.value = !visible.value;
getBannerList();
emits('change');
};
const onConfirm = () => {
if (!selectBanner.value) return false;
loading.value = true;
emits('update', selectBanner.value);
loading.value = false;
onCancel();
};
const onCancel = () => {
visible.value = false;
// bannerList.value = [];
selectBanner.value = null;
};
</script>
<style scoped lang="scss">
$banner-choose-border-color: var(--color-G200);
.banner-choose {
&-trigger {
&:hover {
cursor: pointer;
opacity: 0.9;
}
}
&-contain {
max-height: 500px;
overflow: auto;
padding: 4px 8px;
}
&-content {
display: flex;
flex-direction: column;
gap: 16px;
}
&-row {
padding: 8px 16px;
border: 1px solid transparent;
&.is-selected {
border: 1px solid var(--color-border);
}
&:hover {
cursor: pointer;
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.5);
}
&-title {
font-size: 18px;
font-weight: 500;
color: var(--color-CG1000);
padding-bottom: 4px;
}
&-img {
height: 60px;
> img {
height: 100%;
width: 100%;
object-fit: cover;
}
}
}
&-footer {
display: flex;
align-items: center;
justify-content: flex-end;
padding-top: 24px;
}
&-button {
margin-left: 8px;
padding: 6px 16px;
}
}
</style>

View File

@@ -0,0 +1,178 @@
<template>
<panel v-if="hasRepo" class="md-intro overflow-hidden" :class="{ 'is-mobile': mobile }">
<template #header>
<div class="md-intro-header">
<Icon name="gt-file-c" size="16px" />
<span class="md-intro-title" :class="{ 'is-mobile': mobile }">
<a :href="link" target="_blank">{{ namespace }} / {{ file.file_name || 'README.md' }}</a>
</span>
</div>
</template>
<template #headerRight>
<a v-if="isSelf" :href="link" target="_blank">
<Icon name="gt-edit" size="16px" />
</a>
</template>
<div ref="eleRef" class="md-intro-wrapper" :class="{ 'show-all': showMore }">
<MdRender v-if="hasRepo" v-model="content" :mdStyle="{
height: '100%'
}" class="g-md-container" :custom-plugins="customPlugins" toggle-repo-permission />
<div v-if="isOver && !showMore" class="md-intro-btn" @click="onShowMore">
查看全部
</div>
</div>
</panel>
</template>
<script lang="ts">
export default {
name: 'md-intro'
};
</script>
<script setup lang="ts">
import { ref, onMounted, computed, onUnmounted } from 'vue';
import { useUserInfo } from '@/views/User/hooks/useUserInfo';
import Panel from '@/components/Panel/index.vue';
import { getRepoReadme } from '@/api/repo';
import Base64 from 'crypto-js/enc-base64';
import utf8 from 'crypto-js/enc-utf8';
import MdRender from '@/components/MdRender/index.vue';
import { useShowMore } from '@/utils/hooks/useShowMore';
withDefaults(defineProps<{
mobile?: boolean;
}>(), {
mobile: false
});
// readme介绍
const content = ref<string>(``);
const { isSelf, namespace } = useUserInfo();
const hasRepo = ref(false);
const branch = ref('');
const eleRef = ref(null);
const { isOver, stop, showMore, onShowMore } = useShowMore(eleRef);
onUnmounted(() => {
stop && stop();
});
const link = computed(() => {
return hasRepo.value ? `/${namespace}/${namespace}` : `/user/${namespace}/repos`;
});
const file = ref<any>({});
const customPlugins:any = ref([]);
onMounted(async() => {
const repoId: string = `${namespace}%2F${namespace}`;
hasRepo.value = false;
// const checkRes = await reqCatch(queryUserActivities, {
// author_name: namespace,
// per_page: 20,
// page: 1
// });
// const events = checkRes.data?.data || {};
// if (Object.keys(events).length === 0) {
// content.value = '没有创建项目';
// return false;
// }
// const res = await getRepoBranches({ project_id: repoId });
// if (!res.data || !res.data.data || !res.data.data.content || res.data.data.content.length === 0) {
// content.value = '尚未创建自我介绍';
// return false;
// }
// const defaultData = (res.data.data.content || []).find((item: { name: string; default: boolean }) => item.default);
// branch.value = defaultData.name;
// const repoRes = await reqCatch(() => getRepoFiles({ repoId, ref: defaultData.name, file_path: 'README.md' }));
// if (!repoRes.data || !repoRes.data.data) {
// content.value = '尚未创建自我介绍';
// return false;
// }
const repoRes = await getRepoReadme({ repoId });
if (!repoRes.data?.data?.content) return false;
file.value = repoRes.data.data;
content.value = utf8.stringify(Base64.parse(repoRes.data.data.content)) || '尚未创建自我介绍';
hasRepo.value = true;
// /// 相对路径转绝对路径
const fileBaseURL = (import.meta as any).env.VITE_DOWNLOAD_HOST;
const orgin = window.location.origin;
const imgPrefix = `${fileBaseURL}/${Array.isArray(namespace) ? namespace.join('/') : namespace}/${namespace}/files/${file.value.branch}/`;
const filePrefix = `${orgin}/${Array.isArray(namespace) ? namespace.join('/') : namespace}/${namespace}/blob/${file.value.branch}/`;
const dirPrefix = `${orgin}/${Array.isArray(namespace) ? namespace.join('/') : namespace}/${namespace}/tree/${file.value.branch}/`;
const getRelativePath = () => {
const relativePath:any = file.value?.file_path?.split('/') || [];
relativePath.pop();
return relativePath.length ? `${relativePath.join('/')}/` : '';
};
const relativePath = getRelativePath();
customPlugins.value = [{
pluginName: 'linkCovertPlugin', // 插件名字(固定名称)
opts: {
imageSrcCovert: relativeUrl => `${imgPrefix}${relativePath}${relativeUrl}`,
linkUrlCovert: relativeUrl => {
// @TODO 文件/文件夹路径转换,此处文件/文件夹存在重大缺陷,待修正
if (relativeUrl?.split('/')?.pop()?.includes('.')) {
// 文件(非文件夹)
return `${filePrefix}${relativePath}${relativeUrl}`;
}
return `${dirPrefix}${relativePath}${relativeUrl}`;
}
}
}];
});
</script>
<style scoped lang="scss">
$md-intro-color-second: #2D2D2E;
.md-intro {
margin-bottom: 24px;
&.is-mobile {
margin-bottom: 12px;
}
&-header {
display: flex;
align-items: center;
}
&-title {
margin-left: 20px;
color: $md-intro-color-second;
font-weight: 500;
line-height: 1;
&.is-mobile {
margin-left: 10px;
}
}
&-wrapper {
position: relative;
max-height: 400px;
overflow: hidden;
&.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,133 @@
<template>
<div v-if="!isPrivate" class="my-organs-organizations">
<div class="my-organs-organizations-header flex items-center justify-between">
<div>
<slot name="title">
<Icon name="gt-organizations-c" class="mr-2" />
<span>组织</span>
</slot>
</div>
<div><Icon name="gt-all" class="cursor-pointer" @click="orgMore"></Icon></div>
</div>
<div class="my-organs-organizations-content">
<a
v-for="(item, index) in orgData"
:key="`${item.web_url}_${index}`"
:href="item.web_url"
:title="item.name"
target="_blank"
class="cursor-pointer hover:text-link"
>
<GAvatar
class="my-organs-organizations-avatar mb-1"
:name="item.name"
:src="item.avatar"
:is_round="false"
:width="40"
:height="40"
/>
</a>
<template v-if="orgData.length === 0">
<slot name="noData" :isSelf="isSelf">
<span v-if="isSelf">还没有加入组织快去创建或加入吧</span>
<span v-else>该用户还没有加入组织</span>
</slot>
</template>
</div>
</div>
</template>
<script lang="ts">
export default {
name: 'my-organs'
};
</script>
<script setup lang="ts">
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { reqCatch } from '@/utils/catch';
import { useUserInfo } from '@/views/User/hooks/useUserInfo';
import { getMyGroupsList, getUserOrgans } from '@/api/user';
import { useIsPrivate } from '@/views/User/hooks/useIsPrivate';
const { isPrivate } = useIsPrivate();
const router = useRouter();
const { namespace, isSelf } = useUserInfo();
const props = withDefaults(defineProps<{ username: string }>(), { username: '' });
// 跳转方法
const handleGo = (link: string) => {
if (!link) return;
router.push(link);
};
// 组织
const orgData = ref<{ web_url: string; name: string; avatar: string }[]>([]);
const init = async() => {
getUserOrgans({ user_name: props.username || namespace, page: 1, per_page: 10 }).then((res) => {
const resData = res.data || { content: [] };
const data = resData.content || [];
orgData.value = data;
// orgData.value = data.map((item: { web_url: string; avatar_url: string; name: string }) => {
// return {
// path: item.web_url,
// img: item.avatar_url,
// name: item.name
// };
// });
});
// const params = {
// 'page': 1,
// 'per_page': 10,
// 'search': '',
// 'order_by': '',
// 'sort': ''
// };
// const res = await reqCatch(getMyGroupsList, params);
// if (!res.error) {
// orgData.value = res?.data?.data?.content;
// }
};
function orgMore() {
router.push({ name: 'settingOrganization' });
}
init();
</script>
<style scoped lang="scss">
$my-organs-border-color-second: #e6e6e8;
.my-organs {
&-organizations {
&-header {
margin-bottom: 12px;
display: flex;
align-items: center;
> span {
font-size: 14px;
font-weight: 500;
color: var(--color-G900);
line-height: 20px;
}
}
&-content {
display: flex;
align-content: center;
flex-wrap: wrap;
overflow: hidden;
max-width: 240px;
}
&-img {
margin-right: 8px;
object-fit: cover;
width: 40px;
height: 40px;
display: block;
}
&-avatar {
margin-right: 4px;
:deep(img) {
object-fit: cover;
}
}
}
}
</style>

View File

@@ -0,0 +1,332 @@
<template>
<Icon class="repo-select-modal-trigger" name="gt-edit" size="16px" @click="onChange" />
<d-modal class="repo-select-modal" v-model="visible" :title="title" :style="{ width: mobile ? '320px' : '500px' }">
<div class="repo-select-modal-content">
<div class="repo-select-modal-row">
<div class="repo-select-modal-label">项目列表</div>
<div class="repo-select-modal-item">
<d-editable-select
v-model="selectRepo"
remote
enable-lazy-load
:options="options"
placeholder="请选择项目"
@load-more="onLoadMore"
:remote-method="onInputChange"
:loading="isSearching"
/>
</div>
</div>
<div v-if="repoList.length" class="repo-select-modal-list">
<div class="repo-select-modal-list-header">
<custom-header title="已选项目" :count="repoList.length" font-size="16px" />
</div>
<div class="repo-select-modal-list-content">
<div v-for="item in repoList" :key="item.id" class="repo-select-modal-list-item">
<d-popover :content="item.title" trigger="hover" :position="['top']">
<div class="repo-select-modal-list-name max-w-[90%] truncate">{{ item.title }}</div>
</d-popover>
<div class="repo-select-modal-list-actions">
<d-button :loading="removeLoading" variant="text" @click="onBeforeRemove(item)">
<Icon name="gt-delete" size="16px" />
</d-button>
</div>
</div>
</div>
</div>
<div class="g-modal-footer">
<d-button class="repo-select-modal-button" color="secondary" :loading="loading" @click="visible = false"
>取消</d-button
>
<d-button
class="repo-select-modal-button"
variant="solid"
:loading="loading"
:disabled="isDisabledBtn"
@click="onConfirm"
>确认</d-button
>
</div>
</div>
</d-modal>
<d-modal v-model="warningVisible">
<template #header>
<gc-modal-header>
<span>请确认</span>
</gc-modal-header>
</template>
<p>是否从精选项目中移除此数据</p>
<template #footer>
<div class="g-modal-footer">
<d-button class="repo-select-modal-button" @click="onCancel">取消</d-button>
<d-button class="repo-select-modal-button" variant="solid" color="danger" @click="onRemove(curItem)"
>确认</d-button
>
</div>
</template>
</d-modal>
</template>
<script lang="ts">
export default {
name: 'repo-select-modal'
};
</script>
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import CustomHeader from '@/components/CustomHeader/index.vue';
import { useUserInfo } from '@/views/User/hooks/useUserInfo';
import { useRepoList, type RepoItemData } from '@/views/User/hooks/useRepoList';
import { removeRepoStatus, updateRepoStatus } from '@/api/user';
import { Message } from 'vue-devui/message';
import { useOrgId } from '@/utils/hooks/useOrgId';
import debounce from 'lodash/debounce';
const props = withDefaults(
defineProps<{
title: string;
mobile?: boolean;
setType: string; // 类型- 'group'组织精选项目设置
repoParams: {
repoHandpickList: any;
repoAllList: any;
};
hackReqFunc?: Function; // 传入搜索回调
}>(),
{
title: '项目设置',
mobile: false,
setType: 'profile_project'
}
);
const emits = defineEmits<{ (e: 'update'): void; (e: 'change'): void; (e: 'loadMore'): void }>();
const visible = ref(false);
const isSearching = ref(false);
const { namespace } = useUserInfo();
const { orgId } = useOrgId();
const params =
props.setType === 'profile_project'
? {
profile: {
params: {
username: orgId.value || namespace
}
},
created: {
params: {
order_by: 'last_activity_at',
visibility: 'public',
sort: 'desc',
user_name: orgId.value || namespace,
page: 1,
per_page: 100
}
},
auto: false
}
: { auto: false };
const { repoList, createdRepoList, init: useInit, getCreatedProfileData } = useRepoList(params);
const init =
props.setType === 'profile_project'
? useInit
: () => {
repoList.value = JSON.parse(JSON.stringify(props.repoParams.repoHandpickList));
createdRepoList.value = JSON.parse(JSON.stringify(props.repoParams.repoAllList));
};
watch(
() => props.repoParams,
() => {
init();
},
{ deep: true, flush: 'post' }
);
const options = computed(() => {
const filterIds = repoList.value.map((item) => item.id);
return createdRepoList.value
.filter((item) => {
return !filterIds.includes(item.id);
})
.map((item) => {
return {
label: item.title || '',
value: item.id
// name: item.title || '',
// value: item.id
};
});
});
const selectRepo = ref(null);
const loading = ref(false);
const onChange = () => {
visible.value = !visible.value;
emits('change');
if (createdRepoList.value.length === 0) {
init();
}
};
const onConfirm = () => {
if (!selectRepo.value) return false;
loading.value = true;
updateRepoStatus({
type: props.setType,
group_id: orgId.value || namespace,
resource_id: selectRepo.value
})
.then(() => {
Message({
message: '设置成功',
type: 'success',
bordered: false
});
selectRepo.value = null;
init();
emits('update');
if (props?.hackReqFunc) {
props.hackReqFunc();
} else {
getCreatedProfileData({ ...params?.created?.params, search: '' });
}
})
.catch((err) => {
console.error(err);
})
.finally(() => {
loading.value = false;
});
};
const removeLoading = ref(false);
const curItem = ref<RepoItemData | null>(null);
const warningVisible = ref(false);
const onBeforeRemove = (item: RepoItemData) => {
curItem.value = item;
warningVisible.value = true;
};
const onCancel = () => {
curItem.value = null;
warningVisible.value = false;
};
const onRemove = (item: RepoItemData | null) => {
warningVisible.value = false;
if (removeLoading.value) return false;
if (!item) return false;
removeLoading.value = true;
removeRepoStatus({
type: props.setType,
group_id: orgId.value || namespace,
resource_id: item.id
})
.then(() => {
Message({
message: '移除成功',
type: 'success',
bordered: false
});
init();
emits('update');
})
.catch((err) => {
console.error(err);
})
.finally(() => {
removeLoading.value = false;
});
};
const onLoadMore = () => {
emits('loadMore');
};
const isDisabledBtn = computed(() => !selectRepo.value);
const onInputChange = debounce(async (val: string) => {
isSearching.value = true;
const cb = () => {
/** anymore */
isSearching.value = false;
};
if (props?.hackReqFunc) {
await props.hackReqFunc(val, cb);
} else {
await getCreatedProfileData({
...params.created?.params,
search: val
});
cb();
}
}, 300);
</script>
<style lang="scss">
$repo-select-modal-border-color: var(--color-G200);
.repo-select-modal {
&-trigger {
&:hover {
cursor: pointer;
opacity: 0.9;
}
}
&-content {
}
&-row {
display: flex;
align-items: center;
}
&-label {
padding-right: 8px;
}
&-item {
flex: 1;
overflow: hidden;
}
&-list {
margin-top: 24px;
&-content {
margin-top: 16px;
max-height: 400px;
margin-right: -8px;
padding-right: 8px;
overflow-y: auto;
}
&-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid $repo-select-modal-border-color;
}
}
&-footer {
display: flex;
align-items: center;
justify-content: flex-end;
padding-top: 24px;
}
&-button {
margin-left: 8px;
padding: 6px 16px;
}
.devui-flexible-overlay {
// left: 0 !important;
// right: 0 !important;
width: 75%;
overflow-y: scroll;
}
}
</style>

View File

@@ -0,0 +1,52 @@
<template>
<div class="starred-topic">
<div class="starred-topic-header">
<div class="starred-topic-title">Starred Topic</div>
</div>
<div class="starred-topic-content">
<div v-for="(item, index) in data" :key="`${item.value}_${index}`" class="starred-topic-item">
{{ item.label }}
</div>
</div>
</div>
</template>
<script lang="ts">
export default {
name: 'starred-topic'
};
</script>
<script setup lang="ts">
import { ref } from 'vue';
const data = ref([
{ label: 'Docker', value: 'Docker' },
{ label: 'JupyterNotebook', value: 'JupyterNotebook' },
{ label: 'Git', value: 'Git' },
{ label: 'AI', value: 'AI' },
{ label: 'ChatGPT LLM', value: 'ChatGPT LLM' }
]);
</script>
<style scoped lang="scss">
$starred-topic-color: #000000;
$starred-topic-color-second: #000000;
.starred-topic {
&-header {
margin-bottom: 24px;
}
&-title {
font-size: 20px;
color: $starred-topic-color;
font-weight: 500;
}
&-item {
font-size: 18px;
font-weight: 500;
color: $starred-topic-color-second;
line-height: 1;
margin-bottom: 12px;
}
}
</style>

View File

@@ -0,0 +1,265 @@
<template>
<custom-header v-if="false" class="stars-repos-left-header" title="Starred 项目" :count="starredTotal"></custom-header>
<DataPanel :empty="!starredRepoList?.length" skeleton :loading="starredRepoList?.length ? false : loading">
<div v-loading="loading" :view="{ top: '300px', left: '50%' }" class="stars-repos-content" v-if="starredRepoList?.length">
<template v-for="(item, index) in starredRepoList" :key="item.id">
<repo-item
:iconHandleList="item.iconHandleList"
:id="item.id"
:imgSrc="item.imgSrc"
:title="item.title"
:tag-list="item.tagList"
:desc="item.desc"
:isStar="item.isStar"
:web_url="item.web_url"
:hide-star="!isSelf"
:topic-names="item.topicNames"
:class="{ 'is-last': index === starredRepoList.length - 1 }"
@handle-star="({isStar}) => item.isStar = isStar"
/>
</template>
</div>
</DataPanel>
<teleport v-if="tel" :to="tel">
<div class="stars-repos-filters">
<div class="stars-repos-filter-item filter-item-input">
<d-input
v-model="search"
clearable
placeholder="搜索项目"
class="stars-repos-filter-form-item"
style="width: 100%"
maxlength="100"
@change="onSearch" @clear="onSearch">
<template #prefix>
<Icon name="gt-search" color="#B3B4B5" style="font-size: inherit;" />
</template>
</d-input>
</div>
<div class="stars-repos-filter-item filter-item-selects">
<div class="stars-repos-filter-item">
<d-select
class="stars-repos-filter-select"
v-model="filter.type"
:options="types"
allow-clear
placeholder="类型"
>
</d-select>
</div>
<div class="stars-repos-filter-item">
<d-select
class="stars-repos-filter-select"
v-model="filter.lang"
:options="langList"
allow-clear
placeholder="语言"
>
</d-select>
</div>
<div class="stars-repos-filter-item is-last">
<d-select
class="stars-repos-filter-select last-select"
style="max-width: 120px;"
v-model="filter.sort"
:options="sorts"
allow-clear
placeholder="排序"
>
</d-select>
</div>
</div>
</div>
</teleport>
<div v-show="pager.total > pager.pageSize" class="stars-repos-pagination">
<d-pagination
:total="pager.total"
v-model:pageSize="pager.pageSize"
v-model:pageIndex="pager.pageIndex"
:can-view-total="true"
:can-change-page-size="true"
:max-items="5"
/>
</div>
</template>
<script lang="ts">
export default {
name: 'stars-repos'
};
</script>
<script setup lang="ts">
import { ref, watch } from 'vue';
import RepoItem from '@/components/RepoItem/index.vue';
import { useRepoList } from '@/views/User/hooks/useRepoList';
import { useUserInfo } from '@/views/User/hooks/useUserInfo';
import { REPO_TYPES, SORT_TYPES } from '@/views/User/constant/const';
import CustomHeader from '@/components/CustomHeader/index.vue';
import { useUserLang } from '@/views/User/hooks/useUserLang';
withDefaults(defineProps<{
tel: any;
}>(), {
tel: 'body'
});
const filter = ref({
type: null,
lang: null,
sort: null
});
const types = ref(REPO_TYPES);
const sorts = ref(SORT_TYPES);
const pager = ref({
total: 0,
pageSize: 10,
pageIndex: 1
});
const { isSelf, namespace } = useUserInfo();
const { langList } = useUserLang('starred');
const search = ref('');
const getQueryParams = () => {
return {
starred: {
params: {
user_name: namespace,
page: pager.value.pageIndex,
per_page: pager.value.pageSize,
order_by: filter.value?.sort?.split(':')[0] || '',
sort: filter.value?.sort?.split(':')[1] || '',
search: search.value || null,
visibility: isSelf ? filter.value.type : 'public',
with_programming_language: filter.value.lang || null
}
},
auto: true
};
};
const { starredRepoList, starredTotal, loading, toggleRepoStar, init } = useRepoList(getQueryParams(), getQueryParams);
watch(() => starredTotal.value, (val) => {
pager.value.total = val || 0;
});
watch(() => pager.value.pageIndex, () => {
init(getQueryParams());
}, { deep: true, flush: 'post' });
watch(() => pager.value.pageSize, (val) => {
if (val > 1) {
pager.value.pageIndex = 1;
} else {
init(getQueryParams());
}
}, { deep: true, flush: 'post' });
watch(() => filter.value, () => {
init(getQueryParams());
}, { deep: true, flush: 'post' });
const onSearch = () => {
init(getQueryParams());
};
</script>
<style scoped lang="scss">
$stars-repos-border-color: #808080;
$stars-repos-border-color-second: #E6E7E8;
$stars-repos-border-radius: 4px;
$stars-repos-stars-color: #000000;
.stars-repos {
&-left-header {
display: flex;
align-items: center;
margin-bottom: 24px;
&-title {
display: flex;
align-items: center;
font-size: 20px;
color: $stars-repos-stars-color;
font-weight: 500;
margin-right: 8px;
}
}
&-content {
overflow: hidden;
.is-last {
:deep(.g-repo-item) {
border-bottom: none;
}
}
:deep(.g-repo-item) {
border-top: none;
border-left: none;
border-right: none;
border-radius: 0;
box-shadow: none;
}
}
&-pagination {
padding-top: 20px;
padding-bottom: 20px;
display: flex;
justify-content: center;
}
&-filters {
display: flex;
align-items: center;
flex: 1;
}
&-filter-form-item {
:deep(.devui-input__wrapper) {
border-color: $stars-repos-border-color-second;
}
}
&-filter-item {
margin-right: 0;
&.filter-item-input {
margin-right: 8px;
flex: 1;
overflow: hidden;
}
&.filter-item-selects {
display: flex;
align-items: center;
border: 1px solid $stars-repos-border-color-second;
border-radius: $stars-repos-border-radius;
overflow: hidden;
}
}
&-filter-select {
max-width: 100px;
position: relative;
&::after {
content: '';
height: 14px;
width: 1px;
background-color: $stars-repos-border-color-second;
display: block;
position: absolute;
top: 50%;
right: 0;
z-index: 20;
transform: translateY(-50%);
}
&.last-select {
&::after {
display: none;
}
}
:deep(.devui-select__selection) {
border: none;
border-radius: 0;
}
:deep(.devui-select__input) {
padding-left: 16px;
}
}
}
</style>

View File

@@ -0,0 +1,15 @@
// 关注用户信息
export type FollowUserData = {
id: string;
avatar?: string;
nickname?: string;
username: string;
description?: string;
location?: string;
org?: string;
round?: boolean;
loading?: boolean;
hasStarred?: boolean;
website?: string;
followType?: number;
}

View File

@@ -0,0 +1,99 @@
<template>
<div class="user-follow-list">
<user-item
v-for="(item, index) in data"
:key="`${item.id}_${index}`"
:data="item"
:loading="item.loading"
:show-button="showButton"
:is-first="index === 0"
:is-last="index === data.length - 1"
:keywords="keywords"
@starred="onToggle($event, item)"
/>
</div>
</template>
<script lang="ts">
export default {
name: 'user-follow-list'
};
</script>
<script setup lang="ts">
import UserItem from '@/components/UserItem/index.vue';
import type { FollowUserData } from '@/views/User/components/types';
import { followUser, unfollowUser } from '@/api/user';
import { joinCommunity, unJoinCommunity } from '@/api/org/devIndex';
import { Message } from 'vue-devui/message';
import { useUserInfo } from '@/views/User/hooks/useUserInfo';
withDefaults(defineProps<{
data: FollowUserData[];
showButton?: boolean;
keywords?: string
}>(), {
data: () => ([]),
showButton: true,
keywords: ''
});
const emits = defineEmits<{(e: 'refresh'): void
}>();
const { namespace } = useUserInfo();
const username = useUserInfo().userInfo.username || '';
const onToggle = (follow: boolean, data: FollowUserData) => {
if (data.loading) return false;
data.loading = true;
if (follow) {
if (data.followType === 0) { // 关注用户
followUser({ username: namespace || username, followedUsername: data.username, followType: data.followType || 0 })
.then(() => {
Message({ type: 'success', message: '关注成功' });
emits('refresh');
})
.finally(() => {
data.loading = false;
});
} else if (data.followType === 1) { // 关注组织
joinCommunity({ followType: 1, followedUsername: data.username })
.then(() => {
emits('refresh');
})
.finally(() => {
data.loading = false;
});
}
} else {
if (data.followType === 0) { // 取消关注用户
unfollowUser({ username: namespace, unfollowUsername: data.username, followType: data.followType || 0 })
.then(() => {
Message({ type: 'success', message: '取消关注成功' });
emits('refresh');
})
.finally(() => {
data.loading = false;
});
} else if (data.followType === 1) { // 取消关注组织
unJoinCommunity({ followType: 1, unfollowUsername: data.username })
.then(() => {
emits('refresh');
})
.finally(() => {
data.loading = false;
});
}
}
};
</script>
<style scoped lang="scss">
.user-follow-list {
box-shadow: 0 0 5px 0 rgba(0,0,0,0.1);
border-radius: 3px;
overflow: hidden;
background-color: #fff;
}
</style>

View File

@@ -0,0 +1,94 @@
type Options = {
name: string;
value: any;
}[]
// 项目类型
export const REPO_TYPES: Options = [
{
name: '全部',
value: ''
},
{
name: '私有',
value: 'private'
},
// {
// name: '内部',
// value: 'internal'
// },
{
name: '公开',
value: 'public'
}
];
// 排序方式
export const SORT_TYPES: Options = [
{
name: '名称升序',
value: 'name:asc'
},
{
name: '名称降序',
value: 'name:desc'
},
{
name: '最近创建',
value: 'created_at:desc'
},
{
name: '较早创建',
value: 'created_at:asc'
},
{
name: '最近活跃',
value: 'last_activity_at:desc'
},
{
name: '较早活跃',
value: 'last_activity_at:asc'
}
];
// 操作类型
export const ACTION_TYPES = {
changed: '修改',
joined: '加入',
created: '创建',
opened: '打开',
closed: '关闭',
'commented on': '发布了评论',
'accepted': '接受',
'pushed new': '创建了一个新的',
'pushed to': '推送了一次',
deleted: '删除',
imported: '导入',
destroyed: '销毁',
left: '离开',
follow: '关注',
transfer: '转移'
};
// 目标类型
export const TARGET_TYPES = {
project: '项目',
'merge request': '合并请求',
'milestone': '里程碑',
issue: '问题',
label: '标签',
tag: 'Tag',
branch: '分支',
group: '组织'
};
// 属性类型
export const PROP_TYPES = {
description: '描述',
path: '路径',
namespace: '命名空间',
lfs_enabled: '启用Git LFS',
visibility: '项目可见级别',
default_branch: '项目默认分支',
security: '项目安全级别'
};

View File

@@ -0,0 +1,97 @@
<script lang="ts" setup>
import { useRouter } from 'vue-router';
import { deepMerge } from '@/utils/index';
import { eventsDic, numRend, mapJson } from '@/utils/hooks/useRepoInit';
import EventAdapter from '@/components/EventAdapter/index.vue';
import { useTimeFormat } from '@/utils/hooks/useTimeFormat';
const { formatTimeFromNow } = useTimeFormat();
const router = useRouter();
interface Activity {
[attr:keyof any]:any;
}
defineProps<{
itemData:Activity,
followOrg:Function,
toggleRepoStar:Function
}>();
function naviTo(name: string, params?: any) {
router.push({ name, params });
}
const followDic = { // follow字典
$: 'target_type',
Project: {
$: 'action_name',
star: (num = 0) => `star了${numRend(num)}项目`
},
Group: {
$: 'action_name',
follow: (num = 0) => `关注了${numRend(num)}组织`
},
Release: {
$: 'action_name',
created: (num = 0) => `新建了${numRend(num)}发行版`
}
};
const compseDic = deepMerge(eventsDic, followDic);// 字典合并
const rdEventName = mapJson(compseDic);
</script>
<template>
<div>
<div class="event-repo">
<div class="event-head flex justify-between mb-2">
<div class="flex">
<GAvatar class="mr-2" @click="naviTo('homepage',{namespace:itemData.author.username})" :src="itemData.author.avatar_url" :name="itemData.author.username" :width="44" :height="44"></GAvatar>
<div class="flex flex-col justify-center">
<div class="mb-1"><span class="font-[600] mr-1">{{ itemData.author.name_cn }}</span><span class="text-G700">@{{ itemData.author.username }}</span></div>
<div v-if="rdEventName(itemData,1)" class="font-[600]">{{ rdEventName(itemData,1) }}</div>
</div>
</div>
<div class="flex items-center">
<div class="text-CG600 text-xs"><Icon name="gt-date" class="mr-1"></Icon><span>{{ formatTimeFromNow(itemData.created_at) }}</span>更新</div>
</div>
</div>
<div class="card">
<Card>
<div class="flex justify items-center">
<div class="flex-grow">
<div class="text-xs text-CG600 flex items-center" v-if="itemData.project_name">
<div class="mr-4" >
<span><a :href="itemData._links?.project" style="color: initial;" class="cursor-pointer">{{ itemData.project_name }}</a></span>
</div>
</div>
<div v-if="itemData.action_name !=='star'" class="mt-2">
<EventAdapter :event-data="itemData">
<template #expandType>
<div v-if="itemData.target_type==='Release'" class="merge">
<template v-if="itemData.title">
<div class="flex items-center text-xs mb-1"><span class="font-[600]">{{ itemData.title.name }}</span></div>
<div class="flex items-center text-xs mb-1"><Icon name="gt-tag" size="12px"></Icon><span class="ml-1">{{ itemData.title.tag }}</span></div>
<div class="flex items-center text-xs mb-1"><Icon name="gt-commit" size="12px"></Icon><span class="text-CG600 ml-1">{{ itemData.title.sha }}</span></div>
<div class="flex items-center text-xs mb-1"><Icon name="gt-tooltip" size="12px"></Icon><span class="ml-1">{{ itemData.title.description }}</span></div>
</template>
</div>
</template>
</EventAdapter>
</div>
</div>
<div class="flex-shrink-0">
<div class="pl-[40px] flex items-center" v-if="itemData.target_type==='Group'&&itemData.group">
<d-button v-if="itemData.group.followed" size="sm" class="follow-btn border boder-G400" @click="followOrg(itemData.group)">取消关注</d-button>
<d-button v-else size="sm" class="follow-btn border boder-G400" @click="followOrg(itemData.group)">关注</d-button>
</div>
<d-button v-if="itemData.action_name==='star'&&itemData.project" class="g-repo-item-attention flex" @click.stop="toggleRepoStar({id:itemData.project_id,isStar:itemData.project.stared})">
<Icon :name="itemData.project.stared ? 'gt-starred-c' : 'gt-star'" :color="itemData.project.stared ? '#FFCC00' : undefined" />
<span class="btn-span">{{ itemData.project.stared ? '取消Star' : 'Star' }}</span>
</d-button>
</div>
</div>
</Card>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,66 @@
<script lang="ts" setup>
import { useRouter } from 'vue-router';
import { createRepo } from '@/api/repo';
import { useRepoImport } from '@/views/Repo/hooks/useRepoImport';
import { reqCatchV2 } from '@/utils/catch';
import { useAccountStore } from '@/stores/user';
const { repoData, formRef, formRules, directionRowList, namespaceList, pageInit, handleNameBlur } = useRepoImport();
pageInit();
const router = useRouter();
const user = useAccountStore();
const { username, id } = user.accountInfo;
const submitCreate = async() => {
const valid = await formRef.value.validate();
if (valid) {
const { name, namespace, repoPath, visibility } = repoData;
const params = {
name,
namespace_id: namespace.id,
path: repoPath,
visibility,
create_position: 'dashboard'
};
namespace.id === id && delete params.namespace_id;
const result = await reqCatchV2(function():any { return createRepo(params); });
if (result.data) {
const namespace_t = params.namespace_id ? namespaceList.value.find((v) => v.value === namespace.id).name : username;
router.push({ name: 'repoDashboard', params: { namespace: namespace_t, repoName: repoPath }});// 项目详情
}
}
};
</script>
<template>
<div>
<d-form class="repo G900" layout="vertical" ref="formRef" :rules="formRules" :data="repoData" >
<d-form-item field="name">
<template #label><span class="text-[15px] font-[600]">项目名称</span></template>
<d-input v-model="repoData.name" placeholder="请输入项目名称" @blur="handleNameBlur"></d-input>
</d-form-item>
<d-form-item field="repoPath">
<template #label><span class="text-[15px] font-[600]">项目路径</span></template>
<div class="flex items-center">
<d-select v-model="repoData.namespace.id" :options="namespaceList" style="width:180px;"></d-select>
<div class="mx-2">/</div>
<d-input v-model="repoData.repoPath" style="width:180px;" :maxlength="100"></d-input>
</div>
</d-form-item>
<d-form-item field="visibility" class="pb-4 no-label">
<d-radio-group direction="column" v-model="repoData.visibility">
<d-radio v-for="item in directionRowList" :key="item.value" :value="item.value" class="mt-2">
<Icon :name="item.icon" class="mr-1"></Icon>
{{ item.name }}</d-radio>
</d-radio-group>
</d-form-item>
</d-form>
<div><d-button variant="solid" @click="submitCreate">创建项目</d-button></div>
</div>
</template>
<style lang="scss" scoped>
.no-label{
:deep(.devui-form__label){
display:none;
}
}
</style>

View File

@@ -0,0 +1,48 @@
<script lang="ts" setup>
import { useTimeFormat } from '@/utils/hooks/useTimeFormat';
import { deepMerge } from '@/utils/index';
import { eventsDic, numRend, mapJson } from '@/utils/hooks/useRepoInit';
interface ActivityEvent{
target_type:string;
action_name:string;
created_at:string;
project_name:string;
note?:any;
merge_request_info?:any;
push_data?:any;
target_title?:any;
_links?:any;
}
defineProps<{eventData:ActivityEvent}>();
const { formatTime } = useTimeFormat();
const followDic = { // follow字典
$: 'target_type',
Project: {
$: 'action_name',
star: (num = 0) => `star了${numRend(num)}项目`
},
Group: {
$: 'action_name',
follow: (num = 0) => `关注了${numRend(num)}组织`
},
Release: {
$: 'action_name',
created: (num = 0) => `新建了${numRend(num)}发行版`
}
};
const compseDic = deepMerge(eventsDic, followDic);// 字典合并
const rdEventName = mapJson(compseDic);
</script>
<template>
<div>
<div class="flex items-center">
<Icon name="gt-issue" class="mr-2"></Icon>
<span class="mr-1">{{ rdEventName(eventData,1) }}</span>
<span class="text-G600 text-xs">{{ formatTime(eventData.created_at, 'HH:mm') }}</span>
</div>
<div class="text-xs text-CG600 pl-6 break-all">
<a :href="eventData._links.action_type||eventData._links.project||eventData._links.group" target="_blank" class="hover:text-inherit">{{eventData.project_name||eventData.group?.name}}</a>
</div>
</div>
</template>

View File

@@ -0,0 +1,284 @@
<script lang="ts" setup>
import FastCreatRepo from './FastCreatRepo.vue';
import UserEvent from './UserEvent.vue';
import GText from '@/components/GText/index.vue';
import RepoItem from '@/components/RepoItem/index.vue';
import { eventsTranslate } from '@/utils/hooks/useRepoInit';
import MyOrgans from '@/views/User/components/my-organs.vue';
import { useUserDashboard } from '@/views/User/hooks/useUserDashboard';
import ActivityItem from './ActivityItem.vue';
import { Message } from 'vue-devui/message';
const {
naviTo,
pageInit,
starRecmd,
followRecmd,
starActivity,
followActivity,
repoShowMore,
eventShowMore,
showActivity,
username,
loadingStatus,
operationData,
repoPager,
eventPager,
userInfo,
userRepoList,
userRepoInView,
recommandRepoList,
recommandOrgList,
userEventsList,
userActivityList
} = useUserDashboard();
pageInit();
if (localStorage.getItem('invite_link') === 'tobeConfirm') {
Message.info({
message: '已进入权限申请队列, 等待管理员审核',
duration: 3000,
showClose: true,
onClose: () => {
localStorage.removeItem('invite_link');
}
});
}
</script>
<template>
<div class="user-dashboard flex items-center justify-center w-full">
<div class="flex py-6 w-full">
<div class="user-profile w-[300px] flex-shrink-0">
<div class="profile mb-6">
<div>
<div class="text-2xl text-G900 font-bold mb-1">
<g-text>{{ userInfo.username }}</g-text>
</div>
<div class="text-G700 mb-1">
@<span>{{ userInfo.namespace }}</span>
</div>
<div class="flex items-center">
<router-link :to="`/user/${userInfo.namespace.replace('@', '')}/fans`">
<div class="text-G900">
<span class="font-bold"><Number :number="userInfo.fan_count" /></span><span class="ml-1">粉丝</span>
</div>
</router-link>
<router-link :to="`/user/${userInfo.namespace.replace('@', '')}/following`">
<div class="text-G900 ml-4">
<span class="font-bold"><Number :number="userInfo.follow_count" /></span
><span class="ml-1">关注</span>
</div>
</router-link>
</div>
</div>
</div>
<div class="my-repos pt-6 border-t boder-G200 mb-6">
<div class="flex items-center justify-between">
<div><Icon name="gt-activity-c" class="mr-2"></Icon><span class="text-G900 font-[600]">我的项目</span></div>
<div v-if="userRepoInView.length < repoPager.total">
<Icon name="gt-all" class="cursor-pointer" @click="repoShowMore"></Icon>
</div>
</div>
<div class="mt-2">
<d-skeleton :loading="loadingStatus.myRepoLoading" :rows="6">
<div class="flex items-center justify-center text-G700" v-if="userRepoList.length === 0">
<div class="flex flex-col w-full">
<span>你还未创建项目快去新建吧</span>
<d-button class="btn-create-repo mt-2" @click="naviTo('newRepo', null, { position: 'dashboard' })"
>创建项目</d-button
>
</div>
</div>
<div v-for="repo in userRepoInView" :key="repo.id" class="mb-2">
<a :href="repo.web_url" style="color: initial">
<span
v-for="(dir, idx) in repo.namespace"
:key="idx"
class="break-all"
:class="idx === repo.namespace.length - 1 ? 'font-bold' : ''"
>
{{ idx === repo.namespace.length - 1 ? repo.name : dir
}}<span v-if="idx !== repo.namespace.length - 1" class="px-1">/</span>
</span>
</a>
</div>
</d-skeleton>
</div>
</div>
<div class="my-events pt-6 border-t boder-G200 mb-6">
<div class="flex items-center justify-between">
<div><Icon name="gt-public-c" class="mr-2"></Icon><span class="text-G900 font-[600]">我的动态</span></div>
<div v-if="eventPager.total >= eventPager.pageSize">
<Icon name="gt-all" :operable="true" class="cursor-pointer" @click="eventShowMore"></Icon>
</div>
</div>
<div class="mt-2">
<d-skeleton :loading="loadingStatus.eventLoading" :rows="6">
<div v-for="(eventList, date) in userEventsList" :key="date" class="mb-3">
<div class="pl-6">
<span class="text-G500">{{ date }}</span>
</div>
<div>
<div v-for="event in eventsTranslate(eventList || [])" :key="event.target_id" class="mb-2">
<UserEvent :eventData="event" />
</div>
</div>
</div>
<div v-if="Object.keys(userEventsList).length === 0" class="flex items-center justify-center text-G700">
<span>你最近还没有活跃的动态右侧为你推荐了一些优秀项目快去关注吧</span>
</div>
</d-skeleton>
</div>
</div>
<div class="my-org pt-6 border-t boder-G200 mb-6">
<my-organs class="page-user-organizations" :username="username">
<template #noData>
<div class="mt-2">
<div class="flex items-center justify-center text-G700">
<span>你没有加入或创建的组织右侧为你推荐了一些优秀组织快去关注吧</span>
</div>
</div>
</template>
<template #title>
<Icon name="gt-organizations-c" class="mr-2" />
<span class="text-G900 font-[600]">我的组织</span>
</template>
</my-organs>
</div>
</div>
<div class="recommand flex-1 ml-8">
<div class="ad-render mb-5 rounded-[3px] overflow-hidden" v-if="operationData?.img_url">
<div>
<GLink :href="operationData.link_to" target="_blank">
<img :src="operationData.img_url" alt="" />
</GLink>
</div>
</div>
<div v-loading="loadingStatus.boardLoading" style="min-height: 300px">
<div class="follow-notice mb-5" v-if="showActivity && !loadingStatus.boardLoading">
<div>
<DataPanel
skeleton
:card="false"
:loading="loadingStatus.activityLoading"
:empty="!userActivityList?.length"
>
<!-- eslint-disable-next-line vue/require-v-for-key -->
<div v-for="activity in userActivityList" class="mb-8 active-item">
<ActivityItem :itemData="activity" :toggleRepoStar="starActivity" :followOrg="followActivity" />
</div>
</DataPanel>
</div>
</div>
<template v-else-if="!loadingStatus.boardLoading">
<div class="fast-crate mb-5" v-if="userRepoList.length === 0">
<div class="mb-3">
<div class="text-2xl font-[600] text-G900"><span>快速新建项目</span></div>
</div>
<Card class="overflow-hidden">
<FastCreatRepo />
</Card>
</div>
<div class="repo-recomd mb-5">
<div class="mb-3">
<div class="text-2xl font-[600] text-G900"><span>优质项目推荐</span></div>
</div>
<Card class="overflow-hidden" style="padding: 0px">
<div>
<DataPanel
skeleton
:card="false"
:loading="loadingStatus.repoRecmLoading"
:empty="!recommandRepoList?.length"
>
<div v-for="(item, index) in recommandRepoList" :key="item.id">
<repo-item
:iconHandleList="item.iconHandleList"
:id="item.id"
:imgSrc="item.imgSrc"
:title="item.title + '🔥'"
:tag-list="item.tagList"
:desc="item.desc"
:isStar="item.isStar"
:web_url="item.web_url"
:class="{ 'is-last': index === recommandRepoList.length - 1 }"
@handle-star="({isStar}) => item.isStar = isStar"
/>
</div>
</DataPanel>
</div>
</Card>
</div>
<div class="org-recomd mb-5">
<div class="mb-3">
<div class="text-2xl font-[600] text-G900"><span>优质组织推荐</span></div>
</div>
<Card class="overflow-hidden" style="padding: 0px">
<div>
<DataPanel
skeleton
:card="false"
:loading="loadingStatus.orgRecmLoading"
:empty="!recommandOrgList?.length"
>
<div v-for="(item, index) in recommandOrgList" :key="item.id">
<div
class="flex px-5 py-4"
:class="index === recommandOrgList.length - 1 ? '' : 'border-b border-G300'"
>
<div><img style="width: 30px; height: auto" :src="item.avatar" /></div>
<div class="flex-1 ml-6">
<div class="cursor-pointer" @click="naviTo('homepage', { namespace: item.full_path })">
<span class="font-bold">{{ item.name }}</span>
<span v-if="item.group_extend.cmo_name" class="text-G700 ml-2"
>@{{ item.group_extend.cmo_name }}</span
>
</div>
<div class="mt-2">
<span class="text-G700">{{ item.description }}</span>
</div>
</div>
<div class="pl-[40px] flex items-center">
<d-button
v-if="item.starred"
size="sm"
class="follow-btn border boder-G400"
@click="followRecmd(item)"
>取消关注</d-button
>
<d-button v-else size="sm" class="follow-btn border boder-G400" @click="followRecmd(item)"
>关注</d-button
>
</div>
</div>
</div>
</DataPanel>
</div>
</Card>
</div>
</template>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.btn-create-repo {
width: 180px;
background: linear-gradient(180deg, #fcfcfc 0%, #f8f9fb 100%);
box-shadow: 0px 1px 1px 0px rgba(0, 0, 0, 0.05);
border-radius: 3px;
border: 1px solid #d1d2d4;
}
.is-last {
border-bottom: none;
}
.active-item:last-child {
margin-bottom: 0px;
}
.follow-btn {
background: linear-gradient(180deg, #fcfcfc 0%, #f8f9fb 100%);
box-shadow: 0px 1px 1px 0px rgba(0, 0, 0, 0.05);
border-radius: 3px;
}
</style>

View File

@@ -0,0 +1,106 @@
import { ref } from 'vue';
import { useUserInfo } from '@/views/User/hooks/useUserInfo';
import { queryUserContributes } from '@/api/user';
import dayjs from 'dayjs';
export const useContributes = (year: string, isMobile = false) => {
const { namespace } = useUserInfo();
const refresh = ref(false);
const count = ref(0);
const hideContributes = ref(false);
const [endDate, startDate] = [dayjs().format('YYYY-MM-DD'), dayjs().subtract(4, 'month').format('YYYY-MM')];
const range = isMobile ? [`${startDate}-01`, `${endDate}`] : [`${year}-01-01`, `${year}-12-31`];
// 数据处理
const dataHandler = (map: { [propName: string]: number }) => {
count.value = 0;
const data = [];
for (const key in map) {
count.value += map[key] || 0;
data.push([key, map[key] || 0]);
}
return data;
};
const getContributesData = (curYear: string = year) => {
queryUserContributes({ username: namespace, year: curYear })
.then((res) => {
const resData = res.data || {};
const data = dataHandler(resData);
option.value.series[0].data = data;
if (!isMobile) {
option.value.calendar.range = [data[0][0], data[data.length - 1][0]];
}
refresh.value = true;
setTimeout(() => {
refresh.value = false;
}, 16);
})
.catch((err) => {
if (err && Number(err.error_code) === 404) {
hideContributes.value = true;
}
});
};
const option = ref({
tooltip: {
trigger: 'item',
formatter: (param) => {
return `<b>${param.value[0]}</b> <br/> ${param.value[1]}次贡献`;
}
},
visualMap: {
show: false,
min: 0,
max: 30,
inRange: {
color: ['#f5f5f5', '#beccfa', '#7693f5', '#526ecc', '#344899']
}
},
calendar: {
top: 'top',
right: isMobile ? 0 : 20,
left: isMobile ? 20 : 40,
range: range,
cellSize: 15,
splitLine: {
show: false
},
yearLabel: {
show: false
},
monthLabel: {
nameMap: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
position: 'end'
},
dayLabel: {
firstDay: 1,
fontSize: 10,
nameMap: ['日', '一', ' ', ' ', '四', ' ', ' ']
},
itemStyle: {
borderWidth: 4,
borderRadius: 4,
borderColor: '#fff'
}
},
series: [
{
type: 'heatmap',
coordinateSystem: 'calendar',
data: [],
itemStyle: {
borderRadius: 4
}
}
]
});
return {
option,
refresh,
count,
hideContributes,
getContributesData
};
};

View File

@@ -0,0 +1,17 @@
import { computed } from 'vue';
import { useUserInfo } from '@/views/User/hooks/useUserInfo';
import { otherAccountStore } from '@/stores/user';
export const useIsPrivate = () => {
const { isSelf } = useUserInfo();
const user = otherAccountStore();
const isPrivate = computed(() => {
if (isSelf) return false;
if (user?.accountInfo?.profile && !user.accountInfo.profile.setting_private) return false;
return true;
});
return {
isPrivate
};
};

View File

@@ -0,0 +1,215 @@
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { queryUserCreateRepos, queryUserRepos, queryUserStarredRepos } from '@/api/user';
import { getRepos, starRepo, unstarRepo } from '@/api/repo';
import { useUserInfo } from '@/views/User/hooks/useUserInfo';
import { emitEvent } from '@/utils/eventBus';
import type { RepoItemResData, TagItem } from '@/components/RepoItem/types';
import { dataHandler } from '@/components/RepoItem/datahandle';
type iconHandleList = {
icon: string;
value: number | string;
iconColor?: string;
label: string;
to: string;
}
export type RepoItemData = {
id: string;
imgSrc: string;
title: string;
desc: string;
isStar: boolean;
tagList: TagItem[];
to: string;
web_url: string;
topicNames: string[];
iconHandleList: iconHandleList[];
}
export type ProfileRepoItemResData = {
object_id: string;
type: string;
resource_id: string;
data: RepoItemResData;
}
type InnerParams = {
all?: {
params?: any
};
created?: {
params?: any
};
profile?: {
params: {
username: string;
}
},
auto?: boolean;
}
export const useRepoList = (params: InnerParams, getParams?: () => InnerParams) => {
const repoList = ref<RepoItemData[]>([]);
const allRepoList = ref<RepoItemData[]>([]);
const createdRepoList = ref<RepoItemData[]>([]);
const starredRepoList = ref<RepoItemData[]>([]);
const total = ref<number>(0);
const createdTotal = ref<number>(0);
const starredTotal = ref<number>(0);
const loading = ref(false);
const getProfileData = (params) => {
loading.value = true;
queryUserRepos(params)
.then((res) => {
const { data } = res;
const formatData = data.map((item) => {
return {
...item.data,
object_id: item.object_id,
resource_id: item.resource_id,
type: item.type
};
});
repoList.value = dataHandler(formatData);
})
.finally(() => {
loading.value = false;
});
};
// 获取所有列表
const getAllProfileData = (params, setValue: boolean = true) => {
loading.value = true;
return getRepos(params)
.then((res) => {
const resData = res.data || {};
const data = resData.data;
if (!data && setValue) {
allRepoList.value = [];
return false;
}
const content: RepoItemResData[] = data.content || [];
if (setValue) {
total.value = data.total || 0;
allRepoList.value = dataHandler(content);
} else {
return dataHandler(content);
}
})
.finally(() => {
loading.value = false;
});
};
// 获取当前用户创建项目列表
const getCreatedProfileData = (params) => {
loading.value = true;
return queryUserCreateRepos(params)
.then((res) => {
const data = res.data || {};
if (!data) {
createdRepoList.value = [];
return false;
}
const content: RepoItemResData[] = data.content || [];
createdTotal.value = data.total || 0;
createdRepoList.value = dataHandler(content);
})
.finally(() => {
loading.value = false;
});
};
// 获取当前用户关注项目列表
const getStarredProfileData = (params) => {
loading.value = true;
return queryUserStarredRepos(params)
.then((res) => {
const data = res.data || {};
if (!data) {
starredRepoList.value = [];
return false;
}
const content: RepoItemResData[] = data.content || [];
starredTotal.value = data.total || 0;
starredRepoList.value = dataHandler(content);
})
.catch((err) => {
if (err && Number(err.error_code) === 404) {
const router = useRouter();
router.push({ name: '404' });
}
})
.finally(() => {
loading.value = false;
});
};
// 处理项目star
const { userInfo } = useUserInfo();
const toggleRepoStar = ({ id, isStar }) => {
if (loading.value) return false;
if (!userInfo || !userInfo.username) {
emitEvent('login', { triggerType: 'Star' });
return false;
}
loading.value = true;
if (isStar) {
unstarRepo({ repoId: id })
.then(() => {
init();
}).finally(() => {
setTimeout(() => {
loading.value = false;
}, 200);
});
} else {
starRepo({ repoId: id })
.then(() => {
init();
}).finally(() => {
setTimeout(() => {
loading.value = false;
}, 200);
});
}
};
const init = (initParams: InnerParams = getParams ? getParams() : params) => {
if (initParams.profile) {
getProfileData(initParams.profile.params);
}
if (initParams.all) {
getAllProfileData(initParams.all.params);
}
if (initParams.created) {
getCreatedProfileData(initParams.created.params);
}
if (initParams.starred) {
getStarredProfileData(initParams.starred.params);
}
};
if (params.auto) init();
return {
repoList,
allRepoList,
createdRepoList,
starredRepoList,
total,
createdTotal,
starredTotal,
loading,
getAllProfileData,
getCreatedProfileData,
getStarredProfileData,
toggleRepoStar,
init
};
};

View File

@@ -0,0 +1,137 @@
import { ref } from 'vue';
import {
checkHasFollowed,
followUser,
getUserFollowerCounts,
getUserFollowers,
getUserFollows,
unfollowUser
} from '@/api/user';
import { Message } from 'vue-devui/message';
import { emitEvent } from '@/utils/eventBus';
import { otherAccountStore } from '@/stores/user';
import microApp from '@micro-zoe/micro-app';
type Params = {
username?: string;
namespace: string;
}
export const useStarFollow = (params: Params, auto: boolean) => {
// 粉丝总数
const fanCount = ref<number>(0);
// 关注总数
const followCount = ref<number>(0);
// 是否关注正在查看的用户
const hasFollowed = ref<boolean>(false);
// 加载中
const loading = ref(false);
// 是否隐藏粉丝和关注
const hideFollowData = ref(false);
const { username, namespace } = params;
// 获取粉丝总数
const getFanCount = () => {
getUserFollowers({ username: namespace, followType: '0' })
.then((res) => {
fanCount.value = res.data || 0;
});
};
// 获取关注总数
const getFollowCount = () => {
getUserFollows({ username: namespace })
.then((res) => {
followCount.value = res.data || 0;
});
};
// 获取用户关注和粉丝数
const getAllCounts = () => {
getUserFollowerCounts({ username: namespace })
.then((res) => {
const data = res.data || { fans_count: 0, follow_count: 0 };
fanCount.value = data.fans_count;
followCount.value = data.follow_count;
})
.catch(() => {
hideFollowData.value = true;
});
};
// 判断是否关注此用户
const checkFollowedUser = () => {
if (!username) return false;
checkHasFollowed({ username, otherUsername: namespace, followType: 0 })
.then((res) => {
hasFollowed.value = res.data || false;
otherStore.saveFollowed(hasFollowed.value);
});
};
// 关注/取消关注此用户
const otherStore = otherAccountStore();
const toggleStar = (username: string, followedUsername: string, follow: boolean = true) => {
if (!username) {
emitEvent('login', { triggerType: '关注用户' });
return false;
}
loading.value = true;
if (follow) {
followUser({ username, followedUsername, followType: 0 })
.then(() => {
Message({
type: 'success',
message: '关注成功'
});
otherStore.saveFollowed(true);
microApp.setData('user-center', {
type: 'user_hasFollowed_update',
params: true
});
init();
})
.finally(() => {
loading.value = false;
});
} else {
unfollowUser({ username, unfollowUsername: followedUsername, followType: 0 })
.then(() => {
Message({
type: 'success',
message: '取消关注成功'
});
otherStore.saveFollowed(false);
microApp.setData('user-center', {
type: 'user_hasFollowed_update',
params: false
});
init();
})
.finally(() => {
loading.value = false;
});
}
};
const init = () => {
getAllCounts();
// checkFollowedUser();
};
if (auto) init();
return {
hideFollowData,
followCount,
fanCount,
hasFollowed,
loading,
getFanCount,
getFollowCount,
getAllCounts,
checkFollowedUser,
toggleStar,
init
};
};

View File

@@ -0,0 +1,135 @@
import { useUserInfo } from '@/views/User/hooks/useUserInfo';
import { reqCatch } from '@/utils/catch';
import { queryUserActivitiesNew } from '@/api/user';
import { ref } from 'vue';
import { ACTION_TYPES, TARGET_TYPES } from '@/views/User/constant/const';
import { Message } from 'vue-devui/message';
type ActivityItem = {
time: string;
actions: {
actionId?: number,
action: string|number;
enAction: string;
target: string;
targetType: string;
targetTitle: string;
project: string;
projectLink: string;
actionLink: string;
actionContent: { [proName: string]: any };
icon: string;
status: string;
open: boolean;
mergeInfo?: any;
pushData?: any;
filter_sensitive?:boolean;
}[];
};
const dataHandler = (data): ActivityItem[] => {
const arr: ActivityItem[] = [];
for (const key in data) {
const obj: ActivityItem = {};
obj.time = key;
obj.actions = data[key].map((item) => {
const content = typeof item.title === 'string' && item.title.includes('{') ? JSON.parse(item.title) : {};
return {
actionId: item.action,
action: ACTION_TYPES[item.action_name] || item.action_name,
enAction: item.action_name,
target: item.target_type_format,
targetType: TARGET_TYPES[item.target_type_format] || item.target_type_format || TARGET_TYPES[item.push_data?.ref_type] || item.push_data?.ref_type,
targetTitle: item.target_title || item.group?.name || content?.name,
project: item.project_name,
projectLink: item._links?.project || null,
groupLink: item._links?.group || null,
actionLink: item._links?.action_type || null,
actionContent: content,
icon: 'icon-function-guide',
status: 'color-none',
mergeInfo: item.merge_request_info || {},
pushData: item.push_data || {},
groupData: item.group || {},
filter_sensitive: item.filter_sensitive,
open: !['joined', 'created', 'deleted', 'pushed new', 'pushed to', 'imported', 'follow'].includes(item.action_name)
};
});
obj.actions = obj.actions.filter(val => !val.filter_sensitive && ![19, 21, 22, 23, 24, 25].includes(val.actionId as number));// 过滤敏感信息, 邀请,更改角色
if (obj.actions.length) arr.push(obj);
}
return arr.sort((a, b) => {
return Number(b.time.replaceAll('-', '')) - Number(a.time.replaceAll('-', ''));
});
};
export const useTimelineActivities = (year: string) => {
const { namespace } = useUserInfo();
const activitiesData = ref<ActivityItem[]>([]);
const curPage = ref(1);
const pageSize = ref(10);
const isEnd = ref(false);
const nextPage = ref('');
const allData = ref({});
const getData = async(curYear?: string = year, curDay?: string = null) => {
const res = await reqCatch(queryUserActivitiesNew, {
year: curYear,
day: curDay,
author_name: namespace,
per_page: curDay ? 20 : pageSize.value,
page: curPage.value,
next: nextPage.value || null
});
const resData = res.data || {};
if (!resData.data) {
isEnd.value = true;
return;
}
if (curDay && !resData.data?.events[curDay]) {
isEnd.value = true;
if (!allData.value[curDay]) allData.value[curDay] = [];
const formatData = dataHandler(allData.value);
activitiesData.value = formatData;
return;
}
const data = resData.data?.events
? curDay
? { [curDay]: resData.data.events[curDay] }
: resData.data.events
: {} || {};
for (const key in data) {
if (!allData.value[key]) allData.value[key] = [];
allData.value[key] = [...allData.value[key], ...data[key]];
}
nextPage.value = resData.data?.next;
const formatData = dataHandler(allData.value);
if (!nextPage.value) isEnd.value = true;
activitiesData.value = formatData;
};
const loadMore = (curYear?: string = year, curDay?: string = '') => {
if (isEnd.value) {
Message({
type: 'info',
message: '没有更多数据了'
});
return false;
}
getData(curYear, curDay);
};
const getDayData = (curYear: string, curDay: string) => {
isEnd.value = false;
getData(curYear, curDay);
};
return {
getData,
activitiesData,
loadMore,
getDayData,
isEnd,
allData,
nextPage
};
};

View File

@@ -0,0 +1,271 @@
import { reactive, ref, computed } from 'vue';
import { useRouter } from 'vue-router';
import { useAccountStore } from '@/stores/user';
import { dataHandler } from '@/components/RepoItem/datahandle';
import { getAdvertisement, getActiveOrganization, getSelectedRepo } from '@/api/home/index';
import { followUser, unfollowUser, queryUserActivitiesNew, getUserProfile, getUserFollowerCounts, getUserConcernEvents } from '@/api/user';
import { getRepos, starRepo, unstarRepo } from '@/api/repo';
import { joinCommunity, unJoinCommunity } from '@/api/org/devIndex';
import { reqCatch, reqCatchV2 } from '@/utils/catch';
import { Message } from 'vue-devui/message';
import { eventsTranslate } from '@/utils/hooks/useRepoInit';
import debounce from 'lodash/debounce';
export const useUserDashboard = () => {
const router = useRouter();
const { accountInfo: localUser } = useAccountStore();
const username = localUser.username || '';
const userRepoList = ref<any[]>([]);
const userEventsList = ref<any>({});
const recommandRepoList = ref<any[]>([]);
const recommandOrgList = ref<any[]>([]);
const userRepoInView = ref<any[]>([]);
const operationData = ref<any>({});
const userActivityList = ref<any[]>([]);
const repoViewIndex = ref<number>(5);
const repoPager = reactive({
total: 0,
pageSize: 20,
pageIndex: 1
});
const eventPager = reactive({
total: 0,
pageSize: 10,
pageIndex: 1
});
const userInfo = reactive({
avatar: '',
username: '',
description: '',
namespace: username,
fan_count: 0,
follow_count: 0
});
const showActivity = computed(() =>
userInfo.follow_count !== 0 // || userRepoList.value.length !== 0
);
const loadingStatus = reactive<Record<keyof any, boolean>>({
boardLoading: true,
myRepoLoading: true,
eventLoading: true,
repoRecmLoading: true,
orgRecmLoading: true,
activityLoading: true
});
async function getUserInfo() { // 获取用户基础信息
const usrInfoRes = await reqCatchV2(() => getUserProfile({ username }));
if (usrInfoRes.data) {
const { avatar, nickname, profile } = usrInfoRes.data.data;
userInfo.avatar = avatar;
userInfo.username = nickname;
userInfo.description = profile.description;
}
await fetchFollowNum();
}
async function fetchFollowNum() {
const usrCountRes = await reqCatchV2(() => getUserFollowerCounts({ username }));
if (usrCountRes.data) {
const { fans_count, follow_count } = usrCountRes.data.data;
userInfo.fan_count = fans_count;
userInfo.follow_count = follow_count;
}
}
async function getUserRepos() { // 获取用户的项目列表
const repoParams = {
order_by: 'last_activity_at',
sort: 'desc',
per_page: repoPager.pageSize,
user_created: true
};
loadingStatus.myRepoLoading = true;
const repoRes = await getRepos(repoParams);
loadingStatus.myRepoLoading = false;
if (repoRes.data) {
const { content, total } = repoRes.data.data;
userRepoList.value = content.map(({ namespace, ...others }:
{namespace:string, [x:keyof any]:any}) => ({ namespace: namespace.split('/'), ...others }));
repoPager.total = total;
userRepoInView.value = userRepoList.value.slice(0, repoViewIndex.value);
}
}
// async function getRecentEvents() { // 获取最近的动态信息
// const eventParams = {
// per_page: eventPager.pageSize,
// author_name: username
// };
// loadingStatus.eventLoading = true;
// const eventsRes = await reqCatchV2(() => queryUserActivities(eventParams));
// loadingStatus.eventLoading = false;
// if (eventsRes.data) {
// userEventsList.value = eventsRes.data.data;
// let count = 0;
// for (const date in eventsRes.data.data) {
// count += eventsRes.data.data[date].length;
// }
// eventPager.total = count;
// }
// }
async function getRecentEvents() { // 获取最近的动态信息
const eventParams = {
year: '',
day: '',
page: eventPager.pageIndex,
next: '',
per_page: eventPager.pageSize,
author_name: username
};
loadingStatus.eventLoading = true;
const eventsRes = await reqCatchV2(() => queryUserActivitiesNew(eventParams));
loadingStatus.eventLoading = false;
if (eventsRes.data) {
const events = eventsRes.data.data.events;
const result:Record<keyof any, any> = {};
let count = 0;
for (const date in events) {
count += events[date].length;
result[date] = events[date];
if (count > eventPager.pageSize) {
result[date] = result[date].slice(0, eventPager.pageSize - (count - events[date].length));
break;
}
}
userEventsList.value = result;
eventPager.total = count;
}
}
async function recommandRepos() { // 获取推荐的项目
loadingStatus.repoRecmLoading = true;
const recmdRepoRes = await reqCatchV2(():any => getSelectedRepo());
loadingStatus.repoRecmLoading = false;
if (recmdRepoRes.data) {
recommandRepoList.value = dataHandler(recmdRepoRes.data.data.slice(0, 6));
}
}
async function recommandOrgs() { // 获取推荐的组织
loadingStatus.orgRecmLoading = true;
const recmdOrgRes = await reqCatchV2(():any => getActiveOrganization());
loadingStatus.orgRecmLoading = false;
if (recmdOrgRes.data) {
recommandOrgList.value = recmdOrgRes.data.data.slice(0, 6);
}
}
async function getOperationData() { // 获取运营信息
const adRes = await reqCatchV2(():any => getAdvertisement());
if (adRes.data) {
operationData.value = adRes.data.data;
}
}
async function getUserActivity() { // 用户动态推送
loadingStatus.activityLoading = true;
const actRes = await getUserConcernEvents();
loadingStatus.activityLoading = false;
if (actRes.data) {
const { events } = actRes.data.data;
userActivityList.value = eventsTranslate(events || []);
}
}
const toggleRepoStar = async({ id, isStar }:{id:string, isStar:boolean,}) => { // star项目
if (isStar) {
const res = await reqCatch(unstarRepo, { repoId: id });
if (!res.error) toggleRepoStarStat(id, res.data?.data);
} else {
const res = await reqCatch(starRepo, { repoId: id });
if (!res.error) toggleRepoStarStat(id, res.data?.data);
}
};
// 切换状态修改star数量
const toggleRepoStarStat = (id:string, data:{star_count:number}) => {
const current = recommandRepoList.value.find(e => e.id === id);
if (current) {
current.isStar = !current.isStar;
current.iconHandleList.forEach((element:any) => {
if (element?.icon === 'gt-star') {
if (data?.star_count !== undefined) {
element.value = data?.star_count;
} else {
element.value = element.value || 0;
current.isStar ? element.value++ : element.value--;
}
}
});
}
};
const toggleFollow = async(item:any, key = 'starred') => { // 关注组织
const params = {
username,
followType: 1,
unfollowUsername: item.name,
followedUsername: item.name
};
const followRes = await reqCatchV2(() => item[key] ? unJoinCommunity(params) : joinCommunity(params));
if (followRes.data) {
Message({
type: 'success',
message: `${item[key] ? '取消关注' : '关注'}成功`
});
const current = recommandOrgList.value.find(e => e.id === item.id);
if (current) current.starred = !current.starred;
current.starred && fetchFollowNum();
}
};
const pageInit = async() => { // 页面初始化
loadingStatus.boardLoading = true;
await getUserInfo();
await getUserRepos();
loadingStatus.boardLoading = false;
getRecentEvents();
getOperationData();
recommandRepos();
recommandOrgs();
getUserActivity();
};
const naviTo = (name: string, params?: any, query?: any) => {
router.push({ name, params, query });
};
const repoShowMore = async() => { // 查看更多
naviTo('userRepos', { namespace: username });
};
const eventShowMore = async() => { // 动态查看更多
naviTo('homepage', { namespace: username });
};
const followRecmd = debounce(async(item:any) => { // 推荐组织关注
await toggleFollow(item);// 关注
// await recommandOrgs();// 更新列表
}, 500, { leading: true, trailing: false });
const followActivity = async(item:any) => { // 关注用户组织关注
await toggleFollow(item, 'followed');// 关注
await getUserActivity();// 更新动态
};
const starRecmd = debounce(async(item:any) => { // 推荐项目star
await toggleRepoStar(item);// 关注
// await recommandRepos();// 更新列表
}, 500, { leading: true, trailing: false });
const starActivity = async(item:any) => { // 关注用户项目star
await toggleRepoStar(item);// 关注
await getUserActivity();// 更新动态
};
return {
username,
naviTo,
pageInit,
repoPager,
eventPager,
userInfo,
followRecmd,
followActivity,
starRecmd,
starActivity,
repoShowMore,
eventShowMore,
showActivity,
userRepoList,
operationData,
loadingStatus,
userRepoInView,
toggleRepoStar,
userEventsList,
userActivityList,
recommandOrgList,
recommandRepoList
};
};

View File

@@ -0,0 +1,25 @@
import { useRoute } from 'vue-router';
type UserInfo = {
domain_id?: string;
email?: string;
id?: string;
mobile?: string;
nickname?: string;
username?: string;
};
export const useUserInfo = (): { isSelf: boolean; userInfo: UserInfo; namespace: string } => {
const route = useRoute();
const params = route?.params || {};
const namespace = Array.isArray(params.namespace) ? params.namespace.join('/') : params.namespace;
const userInfoStr = localStorage.getItem('userInfo');
const userInfoStr2 = sessionStorage.getItem('userInfo');
const userInfo: UserInfo = userInfoStr ? JSON.parse(userInfoStr) : userInfoStr2 ? JSON.parse(userInfoStr2) : {};
const isSelf = userInfo.username?.toLowerCase() === namespace?.toLowerCase();
return {
userInfo,
isSelf,
namespace
};
};

View File

@@ -0,0 +1,26 @@
import { ref } from 'vue';
import { getUserLangData } from '@/api/user';
import { useUserInfo } from '@/views/User/hooks/useUserInfo';
export const useUserLang = (langType?: string) => {
const langList = ref<{ name: string; value: string; }[]>([]);
const { namespace } = useUserInfo();
const init = (type: string = langType) => {
getUserLangData({ type, user_name: namespace })
.then((res) => {
const resData = res.data;
if (!resData) return false;
langList.value = resData.map((t) => {
return { name: t, value: t };
});
});
};
init();
return {
langList,
init
};
};

276
src/views/User/index.vue Normal file
View File

@@ -0,0 +1,276 @@
<template>
<div class="page-user">
<d-skeleton :loading="loadingStatus.bgImageLoading" :rows="4">
<header class="page-user-header">
<img v-if="banner" class="page-user-banner" :src="banner || defaultBanner" />
<img v-else class="page-user-banner" src="@/assets/imgs/user-banner.png" />
<!--上传banner-->
<div class="page-user-header-actions" v-if="isSelf && false">
<d-upload class="page-user-header-button" accept=".png,.jpg,.gif,.jpeg" :before-upload="beforeUpload"
@file-select="onBannerUpload">
<Icon class="page-user-header-button-icon" name="gt-upload-cover" size="16px" color="#7E7E80" />
</d-upload>
</div>
<!--选择banner-->
<div class="page-user-header-actions" v-if="isSelf">
<div class="page-user-header-button">
<banner-choose @update="onChooseBanner" />
</div>
</div>
</header>
</d-skeleton>
<section class="page-user-wrapper">
<div class="page-user-info">
<user-info @update-banner="onUpdateBanner" @update-info="onUpdateInfo" />
<my-organs class="page-user-organizations" />
</div>
<div class="page-user-content">
<div class="page-user-intro mt-24 mb-24">
<div class="page-user-intro-md">
<md-intro />
</div>
</div>
<div class="page-user-repos">
<panel :blank="false" class="overflow-hidden">
<template #header>
<div class="page-user-repos-header">
<Icon name="gt-folder-c" size="16px" />
<span class="page-user-repos-title">精选项目</span>
</div>
</template>
<template #headerRight>
<repo-select-modal v-if="isSelf" @update="onUpdate" />
</template>
<DataPanel :empty="!repoList?.length" :loading="repoList?.length ? false : loading" skeleton>
<div class="page-user-repos-content" v-loading="loading">
<template v-for="(item, index) in repoList.slice(0, 6)" :key="item.id">
<repo-item class="page-user-repos-item" :iconHandleList="item.iconHandleList" :id="item.id" :imgSrc="item.imgSrc"
:title="item.title" :tag-list="item.tagList" :desc="item.desc" :isStar="item.isStar" :web_url="item.web_url" :topicNames="item.topicNames"
:class="{ 'is-last': index === repoList.length - 1 }" @handle-star="({isStar}) => item.isStar = isStar" />
</template>
</div>
</DataPanel>
</panel>
</div>
<div class="page-user-activities">
<activity-contributes />
</div>
</div>
</section>
</div>
</template>
<script lang="ts">
export default {
name: 'User'
};
</script>
<script lang="ts" setup>
import { ref, watch, reactive } from 'vue';
import UserInfo from '@/views/User/components/UserInfo.vue';
import MyOrgans from '@/views/User/components/my-organs.vue';
import MdIntro from '@/views/User/components/md-intro.vue';
import ActivityContributes from '@/views/User/components/activity-contributes.vue';
import Panel from '@/components/Panel/index.vue';
import RepoItem from '@/components/RepoItem/index.vue';
import RepoSelectModal from '@/views/User/components/repo-select-modal.vue';
import BannerChoose from '@/views/User/components/banner-choose.vue';
import { useUserInfo } from '@/views/User/hooks/useUserInfo';
import { useRepoList } from '@/views/User/hooks/useRepoList';
import { Message } from 'vue-devui/message';
import { uploadFile, updateUserProfile } from '@/api/user';
import { useRoute, useRouter } from 'vue-router';
import { otherAccountStore } from '@/stores/user';
import { usePageTitle } from '@/utils/hooks/useTitle';
const router = useRouter();
const route = useRoute();
// 精选项目
const { namespace, isSelf } = useUserInfo();
const { repoList, loading, init } = useRepoList({
profile: { params: { username: namespace }},
auto: true
});
const banner = ref('');
const defaultBanner = ref('https://cdn-img.gitcode.com/user-profile/skin-wave.jpg');
const loadingStatus = reactive({
// 加载状态
profileLoading: true,
bgImageLoading: true,
eventsLoading: true,
contributorLoading: true,
releasesLoading: true
});
const onUpdateBanner = (img: string) => {
banner.value = img;
loadingStatus.profileLoading = false;
loadingStatus.bgImageLoading = false;
};
const beforeUpload = (file: any) => {
return false;
};
const infoData = ref<any>(null);
const onUpdateInfo = (data: any) => {
infoData.value = data;
};
const onBannerUpload = async(fileInfo: any) => {
if (fileInfo.length > 0 && fileInfo[0].size > 500 * 1024) {
return Message({
type: 'warning',
message: '图片大小不超过500kb!'
});
}
const resImg: any = await uploadFile(fileInfo, false, fileInfo[0].type);
if (!resImg || resImg.includes('Error')) return false;
if (!infoData.value || !infoData.value.profile) return false;
banner.value = resImg + '?time' + new Date().getTime();
infoData.value.profile.bg_image = resImg;
updateUserProfile(infoData.value);
};
const onChooseBanner = (item: { img: string; title: string }) => {
const resImg = item.img;
banner.value = resImg + '?time' + new Date().getTime();
infoData.value.profile.bg_image = resImg;
updateUserProfile(infoData.value);
};
const onUpdate = () => {
init();
};
watch(
() => route,
(val) => {
if (val.params.namespace !== namespace) {
// 如果从其他用户切换过来导致路由不刷新强制刷新页面
router.go(0);
}
},
{
deep: true
}
);
const user = otherAccountStore();
const userPathInfo = user.accountInfo;
watch(() => userPathInfo, () => {
usePageTitle(userPathInfo.name ? `${userPathInfo.name}(@${userPathInfo.username})` : '个人主页');
}, { deep: true, immediate: true });
</script>
<style scoped lang="scss">
$g-page-user-border-color: #808080;
$g-page-user-border-color-second: #e6e6e8;
$g-page-user-border-radius: 4px;
$g-page-user-border-radius-large: 12px;
$g-page-user-bg-color: #d3d3d3;
$g-page-user-color: #000000;
$g-page-user-color-second: #2d2d2e;
$g-page-user-color-third: #7e7e80;
.page-user {
position: relative;
max-width: 1376px;
margin: 0 auto;
padding-top: 24px;
&-header {
position: relative;
&-button {
position: absolute;
bottom: 10px;
right: 10px;
line-height: 1;
&-icon {
color: $g-page-user-color;
cursor: pointer;
line-height: 1;
}
:deep(.button-content) {
line-height: 1;
}
&:hover {
opacity: 0.9;
}
}
}
&-banner {
object-fit: cover;
width: 100%;
height: 110px;
display: block;
border-top-left-radius: $g-page-user-border-radius-large;
border-top-right-radius: $g-page-user-border-radius-large;
}
&-wrapper {
display: flex;
position: relative;
z-index: 1;
}
&-info {
margin-top: -30px;
}
&-content {
position: relative;
flex: 1;
// overflow: hidden;
// padding-top: 24px;
padding-left: 32px;
}
&-repos {
&-header {
display: flex;
align-items: center;
}
&-title {
font-size: 14px;
font-weight: 500;
color: $g-page-user-color-second;
margin-left: 20px;
}
&-content {
.is-last {
:deep(.g-repo-item) {
border-bottom: none;
}
}
:deep(.g-repo-item) {
border-top: none;
border-left: none;
border-right: none;
border-radius: 0;
box-shadow: none;
}
}
&-item {
:deep(.repo-title):hover {
color: $devui-link;
}
}
}
&-activities {
margin-top: 24px;
}
}
</style>