搜索结果列表页面开发
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>
|
||||
Reference in New Issue
Block a user