Files
Situation-Awareness-Platfor…/src/views/Org/Member/index.vue
2025-03-12 18:41:20 +08:00

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>