搜索结果列表页面开发
This commit is contained in:
178
src/views/User/components/UserInfo.vue
Normal file
178
src/views/User/components/UserInfo.vue
Normal 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>
|
||||
524
src/views/User/components/activity-contributes.vue
Normal file
524
src/views/User/components/activity-contributes.vue
Normal 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>
|
||||
175
src/views/User/components/banner-choose.vue
Normal file
175
src/views/User/components/banner-choose.vue
Normal 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>
|
||||
178
src/views/User/components/md-intro.vue
Normal file
178
src/views/User/components/md-intro.vue
Normal 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>
|
||||
133
src/views/User/components/my-organs.vue
Normal file
133
src/views/User/components/my-organs.vue
Normal 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>
|
||||
332
src/views/User/components/repo-select-modal.vue
Normal file
332
src/views/User/components/repo-select-modal.vue
Normal 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>
|
||||
52
src/views/User/components/starred-topic.vue
Normal file
52
src/views/User/components/starred-topic.vue
Normal 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>
|
||||
265
src/views/User/components/stars-repos.vue
Normal file
265
src/views/User/components/stars-repos.vue
Normal 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>
|
||||
15
src/views/User/components/types.ts
Normal file
15
src/views/User/components/types.ts
Normal 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;
|
||||
}
|
||||
99
src/views/User/components/user-follow-list.vue
Normal file
99
src/views/User/components/user-follow-list.vue
Normal 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>
|
||||
94
src/views/User/constant/const.ts
Normal file
94
src/views/User/constant/const.ts
Normal 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: '项目安全级别'
|
||||
};
|
||||
97
src/views/User/dashboard/ActivityItem.vue
Normal file
97
src/views/User/dashboard/ActivityItem.vue
Normal 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>
|
||||
66
src/views/User/dashboard/FastCreatRepo.vue
Normal file
66
src/views/User/dashboard/FastCreatRepo.vue
Normal 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>
|
||||
48
src/views/User/dashboard/UserEvent.vue
Normal file
48
src/views/User/dashboard/UserEvent.vue
Normal 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>
|
||||
284
src/views/User/dashboard/index.vue
Normal file
284
src/views/User/dashboard/index.vue
Normal 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>
|
||||
106
src/views/User/hooks/useContributes.ts
Normal file
106
src/views/User/hooks/useContributes.ts
Normal 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
|
||||
};
|
||||
};
|
||||
17
src/views/User/hooks/useIsPrivate.ts
Normal file
17
src/views/User/hooks/useIsPrivate.ts
Normal 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
|
||||
};
|
||||
};
|
||||
215
src/views/User/hooks/useRepoList.ts
Normal file
215
src/views/User/hooks/useRepoList.ts
Normal 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
|
||||
};
|
||||
};
|
||||
137
src/views/User/hooks/useStarFollow.ts
Normal file
137
src/views/User/hooks/useStarFollow.ts
Normal 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
|
||||
};
|
||||
};
|
||||
135
src/views/User/hooks/useTimelineActivities.ts
Normal file
135
src/views/User/hooks/useTimelineActivities.ts
Normal 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
|
||||
};
|
||||
};
|
||||
271
src/views/User/hooks/useUserDashboard.ts
Normal file
271
src/views/User/hooks/useUserDashboard.ts
Normal 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
|
||||
};
|
||||
};
|
||||
25
src/views/User/hooks/useUserInfo.ts
Normal file
25
src/views/User/hooks/useUserInfo.ts
Normal 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
|
||||
};
|
||||
};
|
||||
26
src/views/User/hooks/useUserLang.ts
Normal file
26
src/views/User/hooks/useUserLang.ts
Normal 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
276
src/views/User/index.vue
Normal 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>
|
||||
Reference in New Issue
Block a user