aslan-h5/src/views/TeamMemberView.vue
2025-09-28 19:08:35 +08:00

792 lines
19 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="team-member gradient-background-circles">
<MobileHeader :title="`Team Member (${membersTotal})`" />
<div class="content">
<!-- 搜索框 -->
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px">
<div
style="
flex: 1;
border-radius: 32px;
border: 1px solid #fff;
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.25);
padding: 0 8px;
display: flex;
align-items: center;
height: max-content;
"
>
<img src="../assets/icon/search.png" alt="" width="24px" style="opacity: 0.6" />
<input
type="text"
name=""
id=""
placeholder="Please enter the host lD"
style="
width: 100%;
outline: none;
border: none;
background-color: transparent;
color: black;
padding: 10px 8px;
"
v-model="targetUserId"
/>
</div>
<div style="font-weight: 600">Cancel</div>
</div>
<!-- 团队信息 -->
<div
v-if="isFromBdCenter"
style="
background-color: white;
padding: 16px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
display: grid;
grid-template-columns: repeat(3, 1fr);
margin-bottom: 10px;
"
>
<div style="display: flex">
<div style="display: flex; flex-direction: column; align-items: center; gap: 5px">
<div class="team-des-title">Agent number</div>
<div class="team-des-value">{{ agentMonthTarget.agentQuantity }}</div>
</div>
</div>
<div style="display: flex; flex-direction: column; align-items: center; gap: 5px">
<div class="team-des-title">Team target</div>
<div class="team-des-value">${{ agentMonthTarget.thisMonthTotalTarget }}</div>
</div>
<div style="display: flex; justify-content: flex-end">
<div style="display: flex; flex-direction: column; align-items: center; gap: 5px">
<div class="team-des-title">My</div>
<div class="team-des-value">{{ agentMonthTarget.foreverPercentage }}%</div>
</div>
</div>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="loading-container">
<div class="loading-spinner"></div>
<p>Loading members...</p>
</div>
<!-- 成员列表 -->
<div v-if="members.length > 0" class="member-list" ref="memberListRef">
<div
v-for="member in members"
:key="member.id"
style="
background-color: white;
padding: 16px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
justify-content: space-between;
"
>
<!-- 用户信息 -->
<div style="flex: 1; display: flex; align-items: center">
<div
style="
position: relative;
margin-right: 12px;
width: 50px;
height: 50px;
border-radius: 25px;
background-color: #8b5cf6;
color: white;
font-size: 18px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
"
>
<img
v-if="member.userProfile.userAvatar"
:src="member.userProfile.userAvatar"
:alt="member.userProfile.userNickname"
@error="handleImageError"
style="width: 100%; height: 100%; border-radius: 50%; object-fit: cover"
/>
<span
v-else
style="
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-transform: uppercase;
"
>{{ member.userProfile.userNickname.charAt(0) }}</span
>
</div>
<div style="width: calc(100% - 62px); display: flex; flex-direction: column; gap: 4px">
<div
style="
font-weight: 700;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
"
>
{{ member.userProfile.userNickname }}
</div>
<div style="color: rgba(0, 0, 0, 0.4); font-weight: 500">
ID: {{ formatAccount(member.userProfile) }}
</div>
</div>
</div>
<!-- bd展示信息 -->
<div v-if="isFromBdCenter" style="width: 25%; display: flex; justify-content: flex-end">
<div
v-if="member.hostQuantity || member.billBalance"
style="display: flex; flex-direction: column"
>
<div v-if="member.billBalance" style="color: rgba(0, 0, 0, 0.4); font-weight: 510">
${{ member.billBalance }}
</div>
<div
style="
color: rgba(0, 0, 0, 0.4);
font-weight: 510;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
font-size: 0.9em;
"
v-if="member.hostQuantity"
>
Host number:{{ member.hostQuantity }}
</div>
</div>
</div>
<!-- agency展示按钮 -->
<img
v-else
src="/src/assets/icon/trash.png"
alt=""
width="30px"
style="display: block"
@click="trash(member)"
/>
</div>
<!-- 加载提示 -->
<div>
<!-- 加载更多状态 -->
<div v-if="loadingMore" class="loading-more">
<div class="loading-spinner small"></div>
<p>Loading more...</p>
</div>
<!-- 没有更多数据提示 -->
<div v-else-if="noMoreData && members.length > 0" class="no-more-data">
<p>No more members</p>
</div>
</div>
<!-- 无形的加载触发器 -->
<div
v-if="!noMoreData && members.length > 0"
ref="loadMoreTrigger"
class="load-more-trigger"
></div>
</div>
<!-- 空状态 -->
<div v-else class="empty-state">
<div class="empty-icon">👥</div>
<p>No team members found</p>
</div>
</div>
<!-- 删除确认模态框 -->
<div v-if="showDeleteModal" class="modal-overlay" @click="closeDeleteModal">
<div class="delete-modal-content" @click.stop>
<div class="modal-header">
<h3>Tips</h3>
</div>
<div class="modal-body">
<p>
Are you sure you want to remove ID:{{ formatAccount(memberToDelete.userProfile) }} from
the team?
</p>
</div>
<div class="modal-actions">
<button class="cancel-btn" @click="closeDeleteModal">Cancel</button>
<button class="confirm-btn" @click="confirmDelete">Confirm</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, nextTick, onUnmounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import MobileHeader from '../components/MobileHeader.vue'
import {
getTeamMembers,
getBdAgentList,
deleteMember as deleteMemberApi,
getAgentMonthTarget,
} from '../api/teamBill.js'
import { getTeamId } from '@/utils/userStore.js'
import { showError } from '@/utils/toast.js'
const route = useRoute()
const loading = ref(false)
const members = ref([])
const membersTotal = ref(0)
const deletingMemberId = ref(null)
const showDeleteModal = ref(false)
const memberToDelete = ref(null)
const memberListRef = ref(null)
const targetUserId = ref('') //搜索目标id
// 分页相关状态
const loadingMore = ref(false)
const noMoreData = ref(false)
const lastId = ref(null)
const intersectionObserver = ref(null)
const loadMoreTrigger = ref(null)
// 检查是否来自BD Center
const isFromBdCenter = ref(false)
const agentMonthTarget = ref({})
// 获取团队成员列表
const fetchTeamMembers = async (isLoadMore = false) => {
try {
if (isLoadMore) {
loadingMore.value = true
} else {
loading.value = true
// 重新开始时清除分页状态
lastId.value = null
noMoreData.value = false
}
let response
const params = {}
if (lastId.value) {
params.lastId = lastId.value
}
if (isFromBdCenter.value) {
// BD代理商列表也支持分页
response = await getBdAgentList(params)
} else {
// 普通团队成员接口分页
const teamId = getTeamId()
response = await getTeamMembers(teamId, params)
}
if (response.status && response.body) {
const newMembers = response.body || []
if (isLoadMore) {
// 加载更多时,追加到现有列表
members.value = [...members.value, ...newMembers]
} else {
// 初始加载时,替换列表
members.value = newMembers
}
// 更新lastId用于下次分页
if (newMembers.length > 0) {
lastId.value = newMembers[newMembers.length - 1].id
}
// 如果返回的数据少于预期比如少于20条说明没有更多数据
// 这里假设每页显示20条你可以根据实际API调整
if (newMembers.length < 20) {
noMoreData.value = true
}
console.log('Fetched members:', {
isLoadMore,
newCount: newMembers.length,
totalCount: members.value.length,
lastId: lastId.value,
noMoreData: noMoreData.value,
})
} else {
if (!isLoadMore) {
members.value = []
}
noMoreData.value = true
}
} catch (error) {
console.error('Failed to fetch team members:', error)
if (!isLoadMore) {
members.value = []
}
showError('Failed to load team members')
} finally {
loading.value = false
loadingMore.value = false
}
}
// 格式化账户显示
const formatAccount = (userProfile) => {
if (!userProfile) return ''
// 如果有特殊ID优先显示特殊ID
if (userProfile.ownSpecialId && userProfile.ownSpecialId.account) {
return userProfile.ownSpecialId.account
}
// 否则显示普通账户
return userProfile.account || ''
}
// 处理图片加载错误
const handleImageError = (event) => {
event.target.style.display = 'none'
// event.target.nextElementSibling.style.display = 'flex'
}
// 点击删除
const trash = (member) => {
showDeleteModal.value = true
memberToDelete.value = member
}
// 关闭删除模态框
const closeDeleteModal = () => {
showDeleteModal.value = false
memberToDelete.value = null
}
// 确认删除
const confirmDelete = async () => {
if (!memberToDelete.value) return
try {
deletingMemberId.value = memberToDelete.value.id
// 调用删除成员的API接口
await deleteMemberApi([memberToDelete.value.id])
// 从列表中移除成员
const index = members.value.findIndex((m) => m.id === memberToDelete.value.id)
if (index > -1) {
members.value.splice(index, 1)
}
closeDeleteModal()
} catch (error) {
console.error('Failed to delete member:', error)
showError('Failed to delete member. Please try again.')
} finally {
deletingMemberId.value = null
}
}
// 监听滚动事件
const handleScroll = () => {
console.log('handleScroll triggered')
if (loadingMore.value || noMoreData.value) {
console.log('Skip loading:', { loadingMore: loadingMore.value, noMoreData: noMoreData.value })
return
}
const scrollTop = window.pageYOffset || document.documentElement.scrollTop
const windowHeight = window.innerHeight
const documentHeight = document.documentElement.scrollHeight
console.log('Scroll info:', {
scrollTop,
windowHeight,
documentHeight,
distanceToBottom: documentHeight - (scrollTop + windowHeight),
})
// 当滚动到距离底部100px时开始加载
if (scrollTop + windowHeight >= documentHeight - 100) {
console.log('Triggering load more')
fetchTeamMembers(true)
}
}
// 设置 Intersection Observer
const setupIntersectionObserver = () => {
if (!loadMoreTrigger.value) return
intersectionObserver.value = new IntersectionObserver(
(entries) => {
const entry = entries[0]
console.log('Intersection observer triggered:', entry.isIntersecting)
if (entry.isIntersecting && !loadingMore.value && !noMoreData.value) {
console.log('Loading more via intersection observer')
fetchTeamMembers(true)
}
},
{
rootMargin: '100px',
}
)
intersectionObserver.value.observe(loadMoreTrigger.value)
console.log('Intersection observer set up')
}
const getAgentMonth = async () => {
const res = await getAgentMonthTarget()
if (res.status === true) {
agentMonthTarget.value = res.body
}
}
// 组件挂载时获取数据
onMounted(async () => {
console.log('Component mounted')
// 检查是否来自BD Center
isFromBdCenter.value = route.query.source === 'bd-center'
membersTotal.value = parseInt(route.query.members)
if (isFromBdCenter.value) {
getAgentMonth()
}
await fetchTeamMembers()
// 添加滚动监听
await nextTick()
console.log('Adding scroll listener')
window.addEventListener('scroll', handleScroll, { passive: true })
// 也监听 document 的滚动
document.addEventListener('scroll', handleScroll, { passive: true })
// 设置 Intersection Observer
setTimeout(() => {
setupIntersectionObserver()
const windowHeight = window.innerHeight
const documentHeight = document.documentElement.scrollHeight
console.log('Page height check:', {
windowHeight,
documentHeight,
hasScroll: documentHeight > windowHeight,
})
// 如果页面没有滚动条,但是数据不多,可能需要加载更多
if (documentHeight <= windowHeight + 50 && !noMoreData.value && members.value.length > 0) {
console.log('Auto loading more due to insufficient content height')
fetchTeamMembers(true)
}
}, 500)
})
// 组件卸载时清理监听器
onUnmounted(() => {
window.removeEventListener('scroll', handleScroll)
document.removeEventListener('scroll', handleScroll)
if (intersectionObserver.value) {
intersectionObserver.value.disconnect()
}
})
</script>
<style scoped>
* {
color: rgba(0, 0, 0, 0.8);
font-family: 'SF Pro Text';
}
input::placeholder {
font-weight: bold;
color: rgba(0, 0, 0, 0.4);
}
.team-des-title {
font-weight: 600;
}
.team-des-value {
color: rgba(0, 0, 0, 0.4);
font-weight: 500;
}
.team-member {
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
min-height: 100vh;
background-color: #f1f2f3;
}
.content {
padding: 16px;
position: relative;
z-index: 2;
}
/* 加载状态 */
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
padding: 60px 20px;
color: #666;
}
.loading-spinner {
width: 32px;
height: 32px;
border: 3px solid #f3f3f3;
border-top: 3px solid #8b5cf6;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 12px;
}
.loading-spinner.small {
width: 20px;
height: 20px;
border-width: 2px;
margin-bottom: 8px;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* 加载更多状态 */
.loading-more {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
color: #666;
font-size: 14px;
}
/* 没有更多数据提示 */
.no-more-data {
display: flex;
justify-content: center;
padding: 20px;
color: #999;
font-size: 14px;
}
.no-more-data p {
margin: 0;
}
/* 加载触发器 */
.load-more-trigger {
height: 1px;
width: 100%;
margin: 10px 0;
}
/* 成员列表 */
.member-list {
display: flex;
flex-direction: column;
gap: 12px;
}
/* 删除按钮 */
.delete-btn {
background-color: #fee2e2;
border: 1px solid #fecaca;
color: #dc2626;
padding: 8px;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
}
.delete-btn:hover:not(:disabled) {
background-color: #fecaca;
border-color: #fca5a5;
}
.delete-btn:disabled {
cursor: not-allowed;
opacity: 0.6;
}
.delete-btn:active:not(:disabled) {
transform: scale(0.95);
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
color: #666;
text-align: center;
}
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
}
.empty-state p {
margin: 0;
font-size: 16px;
}
/* 删除确认模态框 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 16px;
}
.delete-modal-content {
background-color: white;
border-radius: 12px;
width: 100%;
max-width: 400px;
overflow: hidden;
}
.modal-header {
padding: 20px 20px 0 20px;
}
.modal-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
text-align: center;
}
.modal-body {
padding: 16px 20px;
text-align: center;
}
.modal-body p {
margin: 0 0 8px 0;
font-size: 14px;
color: #666;
line-height: 1.5;
}
.warning-text {
color: #dc2626 !important;
font-weight: 500;
}
.modal-actions {
display: flex;
gap: 12px;
padding: 0 20px 20px 20px;
}
.cancel-btn,
.confirm-btn {
flex: 1;
padding: 12px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 700;
cursor: pointer;
transition: all 0.2s;
}
.cancel-btn {
border: 1px solid #e6e6e6;
background-color: #fff;
color: rgba(0, 0, 0, 0.4);
}
.confirm-btn {
color: rgba(255, 255, 255, 0.8);
background: linear-gradient(
152deg,
rgba(198, 112, 255, 0.6) 7.01%,
rgba(119, 38, 255, 0.6) 92.99%
);
}
.cancel-btn:active,
.confirm-btn:active {
transform: scale(0.98);
}
/* 响应式设计 */
@media (max-width: 480px) {
.delete-btn {
width: 36px;
height: 36px;
font-size: 14px;
}
}
@media screen and (max-width: 360px) {
* {
font-size: 10px;
}
}
@media screen and (min-width: 360px) {
* {
font-size: 12px;
}
}
@media screen and (min-width: 768px) {
* {
font-size: 24px;
}
}
</style>