搜索结果列表页面开发

This commit is contained in:
付民康
2025-03-12 18:41:20 +08:00
commit 7cebe8fc00
739 changed files with 88149 additions and 0 deletions

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,178 @@
<template>
<panel v-if="hasRepo" class="md-intro overflow-hidden" :class="{ 'is-mobile': mobile }">
<template #header>
<div class="md-intro-header">
<Icon name="gt-file-c" size="16px" />
<span class="md-intro-title" :class="{ 'is-mobile': mobile }">
<a :href="link" target="_blank">{{ namespace }} / {{ file.file_name || 'README.md' }}</a>
</span>
</div>
</template>
<template #headerRight>
<a v-if="isSelf" :href="link" target="_blank">
<Icon name="gt-edit" size="16px" />
</a>
</template>
<div ref="eleRef" class="md-intro-wrapper" :class="{ 'show-all': showMore }">
<MdRender v-if="hasRepo" v-model="content" :mdStyle="{
height: '100%'
}" class="g-md-container" :custom-plugins="customPlugins" toggle-repo-permission />
<div v-if="isOver && !showMore" class="md-intro-btn" @click="onShowMore">
查看全部
</div>
</div>
</panel>
</template>
<script lang="ts">
export default {
name: 'md-intro'
};
</script>
<script setup lang="ts">
import { ref, onMounted, computed, onUnmounted } from 'vue';
import { useUserInfo } from '@/views/User/hooks/useUserInfo';
import Panel from '@/components/Panel/index.vue';
import { getRepoReadme } from '@/api/repo';
import Base64 from 'crypto-js/enc-base64';
import utf8 from 'crypto-js/enc-utf8';
import MdRender from '@/components/MdRender/index.vue';
import { useShowMore } from '@/utils/hooks/useShowMore';
withDefaults(defineProps<{
mobile?: boolean;
}>(), {
mobile: false
});
// readme介绍
const content = ref<string>(``);
const { isSelf, namespace } = useUserInfo();
const hasRepo = ref(false);
const branch = ref('');
const eleRef = ref(null);
const { isOver, stop, showMore, onShowMore } = useShowMore(eleRef);
onUnmounted(() => {
stop && stop();
});
const link = computed(() => {
return hasRepo.value ? `/${namespace}/${namespace}` : `/user/${namespace}/repos`;
});
const file = ref<any>({});
const customPlugins:any = ref([]);
onMounted(async() => {
const repoId: string = `${namespace}%2F${namespace}`;
hasRepo.value = false;
// const checkRes = await reqCatch(queryUserActivities, {
// author_name: namespace,
// per_page: 20,
// page: 1
// });
// const events = checkRes.data?.data || {};
// if (Object.keys(events).length === 0) {
// content.value = '没有创建项目';
// return false;
// }
// const res = await getRepoBranches({ project_id: repoId });
// if (!res.data || !res.data.data || !res.data.data.content || res.data.data.content.length === 0) {
// content.value = '尚未创建自我介绍';
// return false;
// }
// const defaultData = (res.data.data.content || []).find((item: { name: string; default: boolean }) => item.default);
// branch.value = defaultData.name;
// const repoRes = await reqCatch(() => getRepoFiles({ repoId, ref: defaultData.name, file_path: 'README.md' }));
// if (!repoRes.data || !repoRes.data.data) {
// content.value = '尚未创建自我介绍';
// return false;
// }
const repoRes = await getRepoReadme({ repoId });
if (!repoRes.data?.data?.content) return false;
file.value = repoRes.data.data;
content.value = utf8.stringify(Base64.parse(repoRes.data.data.content)) || '尚未创建自我介绍';
hasRepo.value = true;
// /// 相对路径转绝对路径
const fileBaseURL = (import.meta as any).env.VITE_DOWNLOAD_HOST;
const orgin = window.location.origin;
const imgPrefix = `${fileBaseURL}/${Array.isArray(namespace) ? namespace.join('/') : namespace}/${namespace}/files/${file.value.branch}/`;
const filePrefix = `${orgin}/${Array.isArray(namespace) ? namespace.join('/') : namespace}/${namespace}/blob/${file.value.branch}/`;
const dirPrefix = `${orgin}/${Array.isArray(namespace) ? namespace.join('/') : namespace}/${namespace}/tree/${file.value.branch}/`;
const getRelativePath = () => {
const relativePath:any = file.value?.file_path?.split('/') || [];
relativePath.pop();
return relativePath.length ? `${relativePath.join('/')}/` : '';
};
const relativePath = getRelativePath();
customPlugins.value = [{
pluginName: 'linkCovertPlugin', // 插件名字(固定名称)
opts: {
imageSrcCovert: relativeUrl => `${imgPrefix}${relativePath}${relativeUrl}`,
linkUrlCovert: relativeUrl => {
// @TODO 文件/文件夹路径转换,此处文件/文件夹存在重大缺陷,待修正
if (relativeUrl?.split('/')?.pop()?.includes('.')) {
// 文件(非文件夹)
return `${filePrefix}${relativePath}${relativeUrl}`;
}
return `${dirPrefix}${relativePath}${relativeUrl}`;
}
}
}];
});
</script>
<style scoped lang="scss">
$md-intro-color-second: #2D2D2E;
.md-intro {
margin-bottom: 24px;
&.is-mobile {
margin-bottom: 12px;
}
&-header {
display: flex;
align-items: center;
}
&-title {
margin-left: 20px;
color: $md-intro-color-second;
font-weight: 500;
line-height: 1;
&.is-mobile {
margin-left: 10px;
}
}
&-wrapper {
position: relative;
max-height: 400px;
overflow: hidden;
&.show-all {
max-height: 100%;
}
}
&-btn {
position: absolute;
left: 50%;
bottom: 0;
transform: translateX(-50%);
z-index: 2;
width: 100%;
text-align: center;
background: #fff;
&:hover {
cursor: pointer;
}
}
}
</style>

View File

@@ -0,0 +1,133 @@
<template>
<div v-if="!isPrivate" class="my-organs-organizations">
<div class="my-organs-organizations-header flex items-center justify-between">
<div>
<slot name="title">
<Icon name="gt-organizations-c" class="mr-2" />
<span>组织</span>
</slot>
</div>
<div><Icon name="gt-all" class="cursor-pointer" @click="orgMore"></Icon></div>
</div>
<div class="my-organs-organizations-content">
<a
v-for="(item, index) in orgData"
:key="`${item.web_url}_${index}`"
:href="item.web_url"
:title="item.name"
target="_blank"
class="cursor-pointer hover:text-link"
>
<GAvatar
class="my-organs-organizations-avatar mb-1"
:name="item.name"
:src="item.avatar"
:is_round="false"
:width="40"
:height="40"
/>
</a>
<template v-if="orgData.length === 0">
<slot name="noData" :isSelf="isSelf">
<span v-if="isSelf">还没有加入组织快去创建或加入吧</span>
<span v-else>该用户还没有加入组织</span>
</slot>
</template>
</div>
</div>
</template>
<script lang="ts">
export default {
name: 'my-organs'
};
</script>
<script setup lang="ts">
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { reqCatch } from '@/utils/catch';
import { useUserInfo } from '@/views/User/hooks/useUserInfo';
import { getMyGroupsList, getUserOrgans } from '@/api/user';
import { useIsPrivate } from '@/views/User/hooks/useIsPrivate';
const { isPrivate } = useIsPrivate();
const router = useRouter();
const { namespace, isSelf } = useUserInfo();
const props = withDefaults(defineProps<{ username: string }>(), { username: '' });
// 跳转方法
const handleGo = (link: string) => {
if (!link) return;
router.push(link);
};
// 组织
const orgData = ref<{ web_url: string; name: string; avatar: string }[]>([]);
const init = async() => {
getUserOrgans({ user_name: props.username || namespace, page: 1, per_page: 10 }).then((res) => {
const resData = res.data || { content: [] };
const data = resData.content || [];
orgData.value = data;
// orgData.value = data.map((item: { web_url: string; avatar_url: string; name: string }) => {
// return {
// path: item.web_url,
// img: item.avatar_url,
// name: item.name
// };
// });
});
// const params = {
// 'page': 1,
// 'per_page': 10,
// 'search': '',
// 'order_by': '',
// 'sort': ''
// };
// const res = await reqCatch(getMyGroupsList, params);
// if (!res.error) {
// orgData.value = res?.data?.data?.content;
// }
};
function orgMore() {
router.push({ name: 'settingOrganization' });
}
init();
</script>
<style scoped lang="scss">
$my-organs-border-color-second: #e6e6e8;
.my-organs {
&-organizations {
&-header {
margin-bottom: 12px;
display: flex;
align-items: center;
> span {
font-size: 14px;
font-weight: 500;
color: var(--color-G900);
line-height: 20px;
}
}
&-content {
display: flex;
align-content: center;
flex-wrap: wrap;
overflow: hidden;
max-width: 240px;
}
&-img {
margin-right: 8px;
object-fit: cover;
width: 40px;
height: 40px;
display: block;
}
&-avatar {
margin-right: 4px;
:deep(img) {
object-fit: cover;
}
}
}
}
</style>

View File

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

View File

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

View File

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

View File

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

View File

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