431 lines
15 KiB
Vue
431 lines
15 KiB
Vue
<template>
|
|
<div class="box-border">
|
|
<Card style="padding:0;">
|
|
<div class="org-member">
|
|
<div class="tabs flex items-center justify-between">
|
|
<div class="tabs-nav flex items-center">
|
|
<div v-for="(item) in tabList" :key="item.id" :id="item.id" class="tabs-nav-option"
|
|
:class="{ active: tableTab === item.id }" @click="updateTable(item)"><span>{{ item.name }}</span></div>
|
|
</div>
|
|
<div class="right-search pr-3">
|
|
<div v-if="tableTab === 'member'" class="flex items-center">
|
|
<d-input v-model="username" placeholder="搜索组织成员" clearable>
|
|
<template #prefix>
|
|
<Icon name="gt-search" />
|
|
</template>
|
|
</d-input>
|
|
<d-button class="ml-4 flex-shrink-0" variant="solid" color="primary" @click="router.push({ name: 'orgSettingMember' })"
|
|
v-if="isAdmin">组织成员管理</d-button>
|
|
</div>
|
|
<div v-if="tableTab === 'follow'" class="flex items-center">
|
|
<d-input v-model="keyword" placeholder="搜索关注用户" clearable @keydown="e=>e.key==='Enter'&&searchFollows()">
|
|
<template #prefix>
|
|
<Icon name="gt-search" @click="searchFollows"/>
|
|
</template>
|
|
</d-input>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-if="tableTab === 'member'">
|
|
<div class="member">
|
|
<DataPanel :loading="loading" :empty="empty" skeleton :card="false" class="my-[12px]">
|
|
<MemberItems class="" :data="userList" showTags isManager :isAdmin="isAdmin" @statusChange="handleChange"
|
|
@removeMember="handleRemove" @exitProject="handleExit" />
|
|
</DataPanel>
|
|
</div>
|
|
</div>
|
|
<div v-if="tableTab === 'follow'">
|
|
<div class="overflow-hidden">
|
|
<DataPanel :loading="followLoading" :empty="followList.length===0" skeleton :card="false" class="my-[12px]">
|
|
<div v-for="item in followList" :key="item.id" class="flex items-center py-4 px-5">
|
|
<GAvatar
|
|
:src="item.avatar"
|
|
:name="item.nickname"
|
|
:width="48"
|
|
:height="48"
|
|
:is_round="true"
|
|
></GAvatar>
|
|
<div class="flex flex-col ml-5 w-[200px]">
|
|
<GText class="text-base">{{ xssPurify(item.nickname) }}</GText>
|
|
<div class="text-CG600 text-sm ellipsis mt-2">@{{ item.username }}</div>
|
|
</div>
|
|
<div class="text-CG600 flex-grow flex justify-center">
|
|
<span>{{formatTime(item.create_time, 'YYYY-MM-DD HH:mm:ss') }}</span>
|
|
</div>
|
|
<div class="w-[100px] flex justify-center">
|
|
<template v-if="localUser.username!==item.username">
|
|
<d-button v-if="item.is_followed" size="sm" class="follow-btn border boder-G400" @click="toggleFollow(item)">取消关注</d-button>
|
|
<d-button v-else size="sm" class="follow-btn border boder-G400" @click="toggleFollow(item)">关注</d-button>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</DataPanel>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
<div v-if="tableTab === 'member'">
|
|
<d-pagination class="px-[20px] py-[20px] flex justify-center" auto-hide size="md" :page-size-options="[10, 20, 50]" :show-page-selector="false"
|
|
:can-view-total="true" :can-change-page-size="true" :max-items="5" :total="pager.total"
|
|
v-model:pageSize="pager.per_page" v-model:pageIndex="pager.page" @page-index-change="getUsers"
|
|
@page-size-change="handleSizeChange" />
|
|
</div>
|
|
<div v-if="tableTab === 'follow'">
|
|
<d-pagination class="px-[20px] py-[20px] flex justify-center" auto-hide size="md" :page-size-options="[10, 20, 50]" :show-page-selector="false"
|
|
:can-view-total="true" :can-change-page-size="true" :max-items="5" :total="followPager.total"
|
|
v-model:pageSize="followPager.per_page" v-model:pageIndex="followPager.page" @page-index-change="getFollowers()"
|
|
@page-size-change="handleFollowSizeChange" />
|
|
</div>
|
|
<GModal v-model="visible" ref="deleteTip" showWarnIcon @confirm="handleConfirm" :title="warningInfo.title">
|
|
<span class="inline-block text-G900 text-sm font-normal leading-[20px] break-all">{{ warningInfo.content }}</span>
|
|
</GModal>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, reactive, onMounted, toRaw, computed, watch, nextTick, onBeforeUnmount } from 'vue';
|
|
import { Message } from 'vue-devui/message';
|
|
import MemberItems from '@/components/MemberItems/index.vue';
|
|
import type { MemberItemData } from '@/components/MemberItems/types';
|
|
import { getOrgMembers, removeGroupUser, exitGroup, setGroupUserLevel } from '@/api/org';
|
|
import { getGroupFollows } from '@/api/org/devIndex';
|
|
import { useAccountStore } from '@/stores/user';
|
|
import { useRouter, useRoute } from 'vue-router';
|
|
import { storeToRefs } from 'pinia';
|
|
import { getOrgInfo } from '@/views/Org/hooks/orgInfo';
|
|
import { orgInfoStore } from '@/stores/Org';
|
|
import setting from '@/setting';
|
|
import { xssPurify } from '@/utils';
|
|
import { GModal } from '@/components/Setting/index';
|
|
import { useTimeFormat } from '@/utils/hooks/useTimeFormat';
|
|
import { reqCatchV2 } from '@/utils/catch';
|
|
import { followUser, unfollowUser } from '@/api/user';
|
|
import { useLoginCheck } from '@/utils/hooks/useLoginCheck';
|
|
|
|
const route = useRoute();
|
|
const router = useRouter();
|
|
const { namespace, getUserCreateOrg } = getOrgInfo();
|
|
const { visitor, developer, admin } = setting.role;
|
|
const loading = ref(true);
|
|
const { isAdmin, communityInfo, access_level } = storeToRefs(orgInfoStore());
|
|
const { formatTime } = useTimeFormat();
|
|
const { loginCheck } = useLoginCheck();
|
|
const empty = computed(() => {
|
|
return !loading.value && !userList.value?.length;
|
|
});
|
|
const hasPermission = computed(() => access_level.value >= visitor);
|
|
const { accountInfo: localUser } = useAccountStore();
|
|
interface RepoGroupUser {
|
|
access_level: number | string
|
|
email: string
|
|
id: number | string
|
|
is_current_source_member: boolean
|
|
name: string
|
|
name_cn?: string
|
|
username: string
|
|
type: string
|
|
web_url: string
|
|
[x: string]: any
|
|
}
|
|
|
|
// 邀请成员
|
|
const visible = ref(false);
|
|
const username = ref('');
|
|
const pager = reactive({
|
|
page: 1,
|
|
per_page: 10,
|
|
total: 0
|
|
});
|
|
const followPager = reactive({
|
|
page: 1,
|
|
per_page: 10,
|
|
total: 0
|
|
});
|
|
const warningInfo = reactive<{ title: string, content: string, name: string, type: 'remove' | 'exit' | '' }>({
|
|
title: '',
|
|
content: '',
|
|
name: '',
|
|
type: ''
|
|
});
|
|
const tableTab = ref(hasPermission.value ? 'member' : 'follow');
|
|
const tabList = hasPermission.value ? [
|
|
{
|
|
id: 'member',
|
|
name: '组织成员'
|
|
}, {
|
|
id: 'follow',
|
|
name: '社区粉丝'
|
|
}
|
|
] : [{
|
|
id: 'follow',
|
|
name: '社区粉丝'
|
|
}];
|
|
const updateTable = (item: any) => {
|
|
tableTab.value = item.id;
|
|
};
|
|
// 判断是否从组织主页banner跳转
|
|
const params = new URLSearchParams(window.location.search);
|
|
const isOrgToFans = params.get('isOrgToFans');
|
|
if (isOrgToFans === 'isOrgToFans') {
|
|
const select = tabList.find(v => v.id === 'follow');
|
|
select && updateTable(select);
|
|
}
|
|
watch(() => route.query, (val) => {
|
|
if (val?.isOrgToFans === 'isOrgToFans') {
|
|
const select = tabList.find(v => v.id === 'follow');
|
|
select && updateTable(select);
|
|
}
|
|
}, { immediate: true, deep: true });
|
|
const keyword = ref('');
|
|
const followLoading = ref(false);
|
|
const followList = ref([]);
|
|
const userList = ref<MemberItemData[]>([]);
|
|
const getUserInfo = (source: RepoGroupUser, content: RepoGroupUser[]) => {
|
|
const userInfo = JSON.parse(localStorage.getItem('userInfo') as string);
|
|
const adminList = content?.filter(item => item.access_level.toString() === '50') || [];
|
|
return {
|
|
imageSrc: source.avatar || source.avatar_url,
|
|
nickname: source.nickname || source.username || '-',
|
|
userName: source.username,
|
|
isSelf: Number(userInfo.arts_id) === Number(source.id),
|
|
isManager: source.access_level.toString() >= admin,
|
|
isDeveloper: source.access_level.toString() >= developer && source.access_level.toString() < admin,
|
|
isVisitor: source.access_level.toString() >= visitor && source.access_level.toString() < developer,
|
|
allowedExit: source.access_level.toString() !== '50' || adminList.length > 1,
|
|
isApplying: false,
|
|
status: source.access_level.toString(),
|
|
lastStatus: source.access_level.toString(),
|
|
path: `/${source.username}`,
|
|
type: source.type,
|
|
join_way: source.join_way,
|
|
disabledOption: false
|
|
};
|
|
};
|
|
|
|
const getUsers = async() => {
|
|
const { page, per_page } = pager;
|
|
const res = await getOrgMembers({ group_id: namespace.value, page, per_page, query: username.value.trim() });
|
|
if (res.status === 200) {
|
|
userList.value = (res.data.content as RepoGroupUser[]).map(item => {
|
|
return getUserInfo(item, res.data.content);
|
|
});
|
|
|
|
pager.total = res?.data?.total;
|
|
pager.page = res?.data?.page_num;
|
|
pager.per_page = res?.data?.page_size;
|
|
}
|
|
loading.value = false;
|
|
};
|
|
const getFollowers = async() => { // 获取组织的粉丝列表
|
|
const { page, per_page } = followPager;
|
|
followLoading.value = true;
|
|
const res = await getGroupFollows({ groupId: namespace.value, nsId: communityInfo.value?.ns_id, keyword: keyword.value, pageNum: page, pageSize: per_page });
|
|
followLoading.value = false;
|
|
if (res.data) {
|
|
const { content, total } = res.data.data;
|
|
followList.value = (content || []).map(({ nick_name, ...other }) => ({ ...other, nickname: nick_name }));
|
|
// if (code === 200) {
|
|
// followList.value = (data.content || []).map(({ gitCodeUser, createdAt, ...other }) => ({
|
|
// ...other,
|
|
// avatar: gitCodeUser.avatar,
|
|
// nickname: gitCodeUser.nickname,
|
|
// username: gitCodeUser.username,
|
|
// create_time: createdAt,
|
|
// is_followed: gitCodeUser.isFollowedForGitCodeUser
|
|
// }));
|
|
// followPager.total = data.total;
|
|
// }
|
|
followPager.total = total;
|
|
}
|
|
};
|
|
const searchFollows = () => {
|
|
followPager.page = 1;
|
|
followPager.total = 0;
|
|
getFollowers();
|
|
};
|
|
const toggleFollow = async(item:any, key = 'is_followed') => { // 关注用户
|
|
if (!loginCheck()) return;
|
|
const params = {
|
|
username: localUser.username || '',
|
|
followType: 0,
|
|
unfollowUsername: item.username,
|
|
followedUsername: item.username
|
|
};
|
|
const followRes = await reqCatchV2(() => item[key] ? unfollowUser(params) : followUser(params));
|
|
if (followRes.data) {
|
|
Message({
|
|
type: 'success',
|
|
message: `${item[key] ? '取消关注' : '关注'}成功`
|
|
});
|
|
await getFollowers();
|
|
}
|
|
};
|
|
const handleRemove = async(config: any) => {
|
|
const { userName } = config.data;
|
|
visible.value = true;
|
|
warningInfo.type = 'remove';
|
|
warningInfo.name = userName;
|
|
warningInfo.title = `你正在移除成员 ${userName}`;
|
|
warningInfo.content = '移除成员后无法恢复, 确认移除么?';
|
|
};
|
|
|
|
const handleConfirm = async() => {
|
|
const { type, name } = warningInfo;
|
|
const config = {
|
|
group_id: namespace.value,
|
|
username: name
|
|
};
|
|
const reqFn = type === 'remove'
|
|
? removeGroupUser
|
|
: exitGroup;
|
|
const res = await reqFn(config);
|
|
if (!res.error) {
|
|
visible.value = false;
|
|
Message.success('操作成功');
|
|
if (type === 'exit') {
|
|
router.replace('/');
|
|
} else {
|
|
for (const key in warningInfo) {
|
|
warningInfo[key as keyof typeof warningInfo] = '';
|
|
}
|
|
getUsers();
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleChange = async(data: any) => {
|
|
const { status, userName, nickname } = toRaw(data.data);
|
|
const cData = await getUserCreateOrg(userName);
|
|
if (status === '50' && !cData) {
|
|
data.data.status = data.data.lastStatus;
|
|
return Message.warning(`${nickname}已经是 5 个组织的管理员,无法再添加为管理员。`);
|
|
}
|
|
const res = await setGroupUserLevel({ group_id: namespace.value, username: userName, access_level: status });
|
|
if (!res.error) {
|
|
Message.success('操作成功');
|
|
getUsers();
|
|
}
|
|
};
|
|
|
|
const handleSearch = async() => {
|
|
pager.total = 0;
|
|
pager.page = 1;
|
|
pager.per_page = 10;
|
|
nextTick(() => {
|
|
getUsers();
|
|
});
|
|
};
|
|
|
|
const setSearch = () => {
|
|
const timer = setTimeout(() => {
|
|
handleSearch();
|
|
}, 200);
|
|
const cancel = () => {
|
|
clearTimeout(timer);
|
|
};
|
|
return {
|
|
cancel
|
|
};
|
|
};
|
|
|
|
const stop = watch(() => username.value.trim(), (_new, _old, onCleanup) => {
|
|
const { cancel } = setSearch();
|
|
onCleanup(cancel);
|
|
});
|
|
const handleExit = () => {
|
|
visible.value = true;
|
|
warningInfo.type = 'exit';
|
|
warningInfo.title = `退出组织?`;
|
|
warningInfo.content = `你确定你想退出吗?你将无法访问所有存储库和团队。`;
|
|
};
|
|
|
|
const handleSizeChange = () => {
|
|
pager.page = 1;
|
|
nextTick(() => {
|
|
getUsers();
|
|
});
|
|
};
|
|
const handleFollowSizeChange = () => {
|
|
followPager.page = 1;
|
|
getFollowers();
|
|
};
|
|
onMounted(() => {
|
|
if (hasPermission.value) getUsers();
|
|
getFollowers();
|
|
});
|
|
// watch(() => communityInfo.value, (val) => {
|
|
// if (val.ns_id) getFollowers();
|
|
// }, { immediate: true });
|
|
onBeforeUnmount(() => {
|
|
stop();
|
|
});
|
|
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.org-member{
|
|
.tabs{
|
|
height: 60px;
|
|
border-bottom: 1px solid var(--color-G300);
|
|
.tabs-nav{
|
|
.tabs-nav-option{
|
|
line-height: 58px;
|
|
color: var(--color-CG600);
|
|
cursor: pointer;
|
|
padding: 0 4px;
|
|
margin: 0 12px;
|
|
&::after{
|
|
content: "";
|
|
display: block;
|
|
margin: auto;
|
|
height: 2px;
|
|
width: 0;
|
|
background: transparent;
|
|
//transition: width var(--devui-animation-duration-slow, .3s) var(--devui-animation-ease-in-out, cubic-bezier(.5, .05, .5, .95)), background-color var(--devui-animation-duration-slow, .3s) var(--devui-animation-ease-in-out, cubic-bezier(.5, .05, .5, .95));
|
|
}
|
|
&.active{color:var(--color-G900);}
|
|
&.active::after{
|
|
content: "";
|
|
display: block;
|
|
margin: auto;
|
|
height: 2px;
|
|
width: 100%;
|
|
background: var(--color-G900);
|
|
//transition: width var(--devui-animation-duration-slow, .3s) var(--devui-animation-ease-in-out, cubic-bezier(.5, .05, .5, .95)), background-color var(--devui-animation-duration-slow, .3s) var(--devui-animation-ease-in-out, cubic-bezier(.5, .05, .5, .95));
|
|
}
|
|
&:hover::after{
|
|
content: "";
|
|
display: block;
|
|
margin: auto;
|
|
height: 2px;
|
|
width: 100%;
|
|
background: var(--color-G900);
|
|
//transition: width var(--devui-animation-duration-slow, .3s) var(--devui-animation-ease-in-out, cubic-bezier(.5, .05, .5, .95)),background-color var(--devui-animation-duration-slow, .3s) var(--devui-animation-ease-in-out, cubic-bezier(.5, .05, .5, .95));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.repo-setting-member-modal {
|
|
width: 370px;
|
|
box-sizing: border-box;
|
|
padding: 0 0 20px;
|
|
|
|
.devui-modal__body {
|
|
padding: 0;
|
|
}
|
|
|
|
.modal-slot-header {
|
|
@apply box-border py-[18px] mx-[22px] text-[var(--color-G900)] text-[16px] border-b-[1px] border-solid border-[var(--color-G200)] font-bold;
|
|
}
|
|
|
|
.devui-flexible-overlay {
|
|
left: 30px !important;
|
|
right: 30px !important;
|
|
max-height: 320px;
|
|
overflow-y: scroll;
|
|
}
|
|
}
|
|
</style>
|