feat:群聊问题修复+相关项目文档更新
This commit is contained in:
@@ -136,7 +136,7 @@ export default {
|
||||
const loadingMore = ref(false)
|
||||
|
||||
const messages = computed(() => chatStore.currentMessages)
|
||||
const hasMore = computed(() => chatStore.hasMoreMap[conversationId.value] !== false)
|
||||
const hasMore = computed(() => conversationId.value > 0 && chatStore.hasMoreMap[conversationId.value] !== false)
|
||||
const isTyping = computed(() => chatStore.typingMap[conversationId.value] || false)
|
||||
const selfAvatar = computed(() => userStore.userInfo?.avatar || '')
|
||||
const selfName = computed(() => userStore.userInfo?.nickname || userStore.userInfo?.username || '我')
|
||||
|
||||
@@ -40,11 +40,12 @@
|
||||
<view class="user-avatar-wrap">
|
||||
<image v-if="user.user_avatar" class="user-avatar" :src="user.user_avatar" mode="aspectFill" />
|
||||
<view v-else class="user-avatar user-avatar-placeholder">
|
||||
<text class="avatar-char">{{ (user.user_nickname || '?')[0] }}</text>
|
||||
<text class="avatar-char">{{ (user.group_nickname || user.user_nickname || '?')[0] }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="user-info">
|
||||
<text class="user-name">{{ user.user_nickname || '未知用户' }}</text>
|
||||
<text class="user-name">{{ user.group_nickname || user.user_nickname || '未知用户' }}</text>
|
||||
<text v-if="user.group_nickname" class="user-real-name">{{ user.user_nickname }}</text>
|
||||
<text v-if="user.read_at && activeTab === 'read'" class="user-time">{{ user.read_at }}</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -87,7 +88,7 @@ export default {
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[ReadDetail] 获取已读详情失败', e)
|
||||
uni.showToast({ title: e?.data?.message || '获取已读详情失败', icon: 'none' })
|
||||
uni.showToast({ title: e?.message || '获取已读详情失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -163,9 +164,10 @@ export default {
|
||||
background-color: #2563EB;
|
||||
}
|
||||
.avatar-char { color: #FFFFFF; font-size: 28rpx; font-weight: 600; }
|
||||
.user-info { flex: 1; }
|
||||
.user-name { font-size: 28rpx; color: #1E293B; }
|
||||
.user-time { display: block; font-size: 22rpx; color: #94A3B8; margin-top: 4rpx; }
|
||||
.user-info { flex: 1; display: flex; flex-direction: column; gap: 4rpx; }
|
||||
.user-name { font-size: 28rpx; color: #1E293B; font-weight: 500; }
|
||||
.user-real-name { font-size: 22rpx; color: #94A3B8; }
|
||||
.user-time { font-size: 22rpx; color: #94A3B8; }
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state { padding: 80rpx 0; text-align: center; }
|
||||
|
||||
@@ -113,6 +113,7 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import { useContactStore } from '@/store/contact'
|
||||
import { useWebSocketStore } from '@/store/websocket'
|
||||
import { useUserStore } from '@/store/user'
|
||||
@@ -134,22 +135,33 @@ const filteredFriends = computed(() => {
|
||||
)
|
||||
})
|
||||
|
||||
const refreshData = async () => {
|
||||
if (!userStore.isLoggedIn) return
|
||||
try {
|
||||
await Promise.all([
|
||||
contactStore.fetchFriends(),
|
||||
contactStore.fetchPendingRequests()
|
||||
])
|
||||
} catch (e) {
|
||||
console.error('获取联系人数据失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (userStore.isLoggedIn) {
|
||||
wsStore.connect()
|
||||
contactStore.initWsListeners()
|
||||
try {
|
||||
await Promise.all([
|
||||
contactStore.fetchFriends(),
|
||||
contactStore.fetchPendingRequests()
|
||||
])
|
||||
} catch (e) {
|
||||
console.error('获取联系人数据失败', e)
|
||||
}
|
||||
await refreshData()
|
||||
}
|
||||
loading.value = false
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
if (!loading.value) {
|
||||
refreshData()
|
||||
}
|
||||
})
|
||||
|
||||
const goToSearch = () => uni.navigateTo({ url: '/pages/contact/search' })
|
||||
const goToRequests = () => uni.navigateTo({ url: '/pages/contact/request' })
|
||||
const goToDetail = (friend) => uni.navigateTo({ url: `/pages/contact/detail?userId=${friend.user_id}` })
|
||||
|
||||
@@ -97,7 +97,7 @@ const handleAccept = async (requestId) => {
|
||||
uni.showToast({ title: '已接受', icon: 'success' })
|
||||
} catch (e) {
|
||||
console.error('接受好友申请失败', e)
|
||||
uni.showToast({ title: e?.data?.message || '接受申请失败', icon: 'none' })
|
||||
uni.showToast({ title: e?.message || '接受申请失败', icon: 'none' })
|
||||
} finally {
|
||||
processingMap[requestId] = false
|
||||
}
|
||||
@@ -111,7 +111,7 @@ const handleReject = async (requestId) => {
|
||||
uni.showToast({ title: '已拒绝', icon: 'none' })
|
||||
} catch (e) {
|
||||
console.error('拒绝好友申请失败', e)
|
||||
uni.showToast({ title: e?.data?.message || '拒绝申请失败', icon: 'none' })
|
||||
uni.showToast({ title: e?.message || '拒绝申请失败', icon: 'none' })
|
||||
} finally {
|
||||
processingMap[requestId] = false
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ const showAddDialog = (user) => {
|
||||
console.error('发送好友申请失败', e)
|
||||
addingMap[user.id] = false
|
||||
uni.showToast({
|
||||
title: e?.data?.message || '发送申请失败',
|
||||
title: e?.message || '发送申请失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -147,9 +147,9 @@
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view v-else class="avatar-img avatar-placeholder avatar-peer at-avatar">
|
||||
<text class="avatar-char">{{ (member.nickname || member.username || '?')[0] }}</text>
|
||||
<text class="avatar-char">{{ (member.nickname || member.user_nickname || '?')[0] }}</text>
|
||||
</view>
|
||||
<text class="at-member-name">{{ member.nickname || member.username }}</text>
|
||||
<text class="at-member-name">{{ member.nickname || member.user_nickname }}</text>
|
||||
<text v-if="member.role === GROUP_ROLE.OWNER" class="at-role-tag">群主</text>
|
||||
<text v-else-if="member.role === GROUP_ROLE.ADMIN" class="at-role-tag">管理</text>
|
||||
</view>
|
||||
@@ -253,7 +253,7 @@ export default {
|
||||
|
||||
const getMemberName = (userId) => {
|
||||
const member = memberMap.value[userId]
|
||||
if (member) return member.nickname || member.username || '未知'
|
||||
if (member) return member.nickname || member.user_nickname || '未知'
|
||||
return '未知成员'
|
||||
}
|
||||
|
||||
@@ -262,16 +262,17 @@ export default {
|
||||
return member ? member.avatar : ''
|
||||
}
|
||||
|
||||
/** 群聊已读标签:仅自己的消息显示 "X人已读" */
|
||||
/** 群聊已读标签:自己发送的消息始终显示 "X人已读"(含 0 人) */
|
||||
const getReadLabel = (msg) => {
|
||||
if (msg.status === 2 || msg._sending || msg._failed || !msg.id) return ''
|
||||
const count = chatStore.groupReadCountMap[msg.id]
|
||||
return count > 0 ? `${count}人已读` : ''
|
||||
if (msg.sender_id !== myId.value) return ''
|
||||
const count = chatStore.groupReadCountMap[msg.id] || 0
|
||||
return `${count}人已读`
|
||||
}
|
||||
|
||||
// ==================== 生命周期 ====================
|
||||
|
||||
onLoad((query) => {
|
||||
onLoad(async (query) => {
|
||||
conversationId.value = parseInt(query.conversationId) || 0
|
||||
groupId.value = parseInt(query.groupId) || 0
|
||||
groupName.value = decodeURIComponent(query.peerName || '')
|
||||
@@ -285,6 +286,13 @@ export default {
|
||||
loadInitialMessages()
|
||||
}
|
||||
|
||||
if (!groupId.value && conversationId.value) {
|
||||
const conv = chatStore.conversationList.find(c => c.id === conversationId.value)
|
||||
if (conv && conv.group_id) {
|
||||
groupId.value = conv.group_id
|
||||
}
|
||||
}
|
||||
|
||||
if (groupId.value) {
|
||||
groupStore.fetchMembers(groupId.value)
|
||||
groupStore.fetchGroupDetail(groupId.value)
|
||||
@@ -359,7 +367,7 @@ export default {
|
||||
inputText.value = inputText.value + '所有人 '
|
||||
} else {
|
||||
atUserIds.value.push(member.user_id)
|
||||
const name = member.nickname || member.username || ''
|
||||
const name = member.nickname || member.user_nickname || ''
|
||||
inputText.value = inputText.value + name + ' '
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,14 +52,14 @@
|
||||
<!-- 搜索好友 -->
|
||||
<view class="section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">选择好友</text>
|
||||
<text class="section-title">选择成员</text>
|
||||
</view>
|
||||
<view class="search-bar">
|
||||
<uni-icons type="search" size="18" color="#94A3B8" />
|
||||
<input
|
||||
class="search-input"
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索好友昵称"
|
||||
placeholder="搜索好友或用户名"
|
||||
placeholder-style="color: #94A3B8"
|
||||
/>
|
||||
<view v-if="searchKeyword" class="search-clear" @tap="searchKeyword = ''">
|
||||
@@ -99,10 +99,42 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else class="empty-state">
|
||||
<view v-else-if="!showSearchSection" class="empty-state">
|
||||
<text class="empty-text">{{ searchKeyword ? '未找到匹配的好友' : '暂无好友' }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 非好友搜索结果 -->
|
||||
<view v-if="showSearchSection" class="section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">搜索到的其他用户</text>
|
||||
</view>
|
||||
<view v-if="searching" class="loading-tip">
|
||||
<text class="loading-tip-text">搜索中...</text>
|
||||
</view>
|
||||
<view v-else-if="nonFriendResults.length > 0" class="friend-list">
|
||||
<view
|
||||
v-for="user in nonFriendResults"
|
||||
:key="user.id"
|
||||
class="friend-item"
|
||||
@tap="toggleSelect({ user_id: user.id, nickname: user.nickname, username: user.username, avatar: user.avatar })"
|
||||
>
|
||||
<view class="checkbox" :class="{ 'checkbox--checked': isSelected(user.id) }">
|
||||
<text v-if="isSelected(user.id)" class="checkbox-icon">✓</text>
|
||||
</view>
|
||||
<view class="avatar" :style="{ backgroundColor: getAvatarColor(user.nickname || user.username) }">
|
||||
<text class="avatar-text">{{ getInitial(user.nickname || user.username) }}</text>
|
||||
</view>
|
||||
<view class="friend-info">
|
||||
<text class="friend-name">{{ user.nickname || user.username }}</text>
|
||||
<text class="friend-account non-friend-tag">非好友</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="empty-state">
|
||||
<text class="empty-text">未找到其他匹配用户</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部占位 -->
|
||||
<view class="bottom-spacer"></view>
|
||||
|
||||
@@ -122,10 +154,11 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { useContactStore } from '@/store/contact'
|
||||
import { useGroupStore } from '@/store/group'
|
||||
import userApi from '@/api/user'
|
||||
import { getAvatarColor, getInitial } from '@/utils/avatar'
|
||||
|
||||
export default {
|
||||
@@ -136,14 +169,18 @@ export default {
|
||||
|
||||
const groupName = ref('')
|
||||
const searchKeyword = ref('')
|
||||
const selectedIds = ref({})
|
||||
const selectedMap = ref({})
|
||||
const loading = ref(true)
|
||||
const submitting = ref(false)
|
||||
const searchedUsers = ref([])
|
||||
const searching = ref(false)
|
||||
let searchTimer = null
|
||||
|
||||
/** 已选中的好友列表 */
|
||||
const selectedList = computed(() => {
|
||||
return contactStore.friendList.filter(f => selectedIds.value[f.user_id])
|
||||
})
|
||||
/** 已选中的用户列表(好友 + 搜索出的非好友) */
|
||||
const selectedList = computed(() => Object.values(selectedMap.value))
|
||||
|
||||
/** 好友 ID 集合 */
|
||||
const friendIdSet = computed(() => new Set(contactStore.friendList.map(f => f.user_id)))
|
||||
|
||||
/** 搜索过滤后的好友列表 */
|
||||
const filteredFriends = computed(() => {
|
||||
@@ -157,34 +194,71 @@ export default {
|
||||
})
|
||||
})
|
||||
|
||||
/** 是否满足创建条件:群名非空 + 至少选2人 */
|
||||
const canCreate = computed(() => {
|
||||
return groupName.value.trim().length > 0 && selectedList.value.length >= 2
|
||||
/** 搜索到的非好友用户(已在好友列表中的去重) */
|
||||
const nonFriendResults = computed(() => {
|
||||
return searchedUsers.value.filter(u => !friendIdSet.value.has(u.id))
|
||||
})
|
||||
|
||||
/** 判断好友是否已选中 */
|
||||
/** 是否显示全站搜索区块 */
|
||||
const showSearchSection = computed(() => {
|
||||
return searchKeyword.value.trim().length > 0 && (nonFriendResults.value.length > 0 || searching.value)
|
||||
})
|
||||
|
||||
/** 是否满足创建条件 */
|
||||
const canCreate = computed(() => {
|
||||
return groupName.value.trim().length > 0 && selectedList.value.length >= 1
|
||||
})
|
||||
|
||||
/** 判断用户是否已选中 */
|
||||
const isSelected = (userId) => {
|
||||
return !!selectedIds.value[userId]
|
||||
return !!selectedMap.value[userId]
|
||||
}
|
||||
|
||||
/** 切换好友选中状态 */
|
||||
const toggleSelect = (friend) => {
|
||||
const newMap = { ...selectedIds.value }
|
||||
if (newMap[friend.user_id]) {
|
||||
delete newMap[friend.user_id]
|
||||
const newMap = { ...selectedMap.value }
|
||||
const uid = friend.user_id || friend.id
|
||||
if (newMap[uid]) {
|
||||
delete newMap[uid]
|
||||
} else {
|
||||
newMap[friend.user_id] = true
|
||||
newMap[uid] = {
|
||||
user_id: uid,
|
||||
nickname: friend.nickname || friend.username || '',
|
||||
username: friend.username || '',
|
||||
remark: friend.remark || '',
|
||||
avatar: friend.avatar || ''
|
||||
}
|
||||
}
|
||||
selectedIds.value = newMap
|
||||
selectedMap.value = newMap
|
||||
}
|
||||
|
||||
/** 搜索关键词变化时延迟搜索全站用户 */
|
||||
watch(searchKeyword, (val) => {
|
||||
if (searchTimer) clearTimeout(searchTimer)
|
||||
const kw = val.trim()
|
||||
if (kw.length < 2) {
|
||||
searchedUsers.value = []
|
||||
return
|
||||
}
|
||||
searchTimer = setTimeout(async () => {
|
||||
searching.value = true
|
||||
try {
|
||||
const res = await userApi.searchUsers(kw, 1, 20)
|
||||
searchedUsers.value = res.data?.list || []
|
||||
} catch {
|
||||
searchedUsers.value = []
|
||||
}
|
||||
searching.value = false
|
||||
}, 400)
|
||||
})
|
||||
|
||||
/** 提交创建群聊 */
|
||||
const handleCreate = async () => {
|
||||
if (!canCreate.value || submitting.value) return
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
const memberIds = selectedList.value.map(f => f.user_id)
|
||||
const memberIds = selectedList.value.map(f => f.user_id || f.id)
|
||||
const result = await groupStore.createGroup(groupName.value.trim(), memberIds)
|
||||
|
||||
uni.showToast({ title: '群聊创建成功', icon: 'success' })
|
||||
@@ -192,7 +266,7 @@ export default {
|
||||
setTimeout(() => {
|
||||
if (result && result.conversation_id) {
|
||||
uni.redirectTo({
|
||||
url: `/pages/group/conversation?conversationId=${result.conversation_id}&groupId=${result.group_id || 0}&peerName=${encodeURIComponent(groupName.value.trim())}`
|
||||
url: `/pages/group/conversation?conversationId=${result.conversation_id}&groupId=${result.id || 0}&peerName=${encodeURIComponent(groupName.value.trim())}`
|
||||
})
|
||||
} else {
|
||||
uni.navigateBack()
|
||||
@@ -201,7 +275,7 @@ export default {
|
||||
} catch (e) {
|
||||
console.error('创建群聊失败', e)
|
||||
uni.showToast({
|
||||
title: e?.data?.message || '创建群聊失败',
|
||||
title: e?.message || '创建群聊失败',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
@@ -223,6 +297,9 @@ export default {
|
||||
searchKeyword,
|
||||
selectedList,
|
||||
filteredFriends,
|
||||
nonFriendResults,
|
||||
showSearchSection,
|
||||
searching,
|
||||
canCreate,
|
||||
loading,
|
||||
submitting,
|
||||
@@ -518,6 +595,25 @@ export default {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.non-friend-tag {
|
||||
color: #D97706;
|
||||
font-size: 22rpx;
|
||||
background-color: #FFFBEB;
|
||||
padding: 2rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.loading-tip {
|
||||
padding: 40rpx 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.loading-tip-text {
|
||||
font-size: 26rpx;
|
||||
color: #94A3B8;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
padding: 80rpx 0;
|
||||
|
||||
@@ -32,17 +32,17 @@
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 搜索好友 -->
|
||||
<!-- 搜索成员 -->
|
||||
<view class="section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">选择好友</text>
|
||||
<text class="section-title">选择成员</text>
|
||||
</view>
|
||||
<view class="search-bar">
|
||||
<uni-icons type="search" size="18" color="#94A3B8" />
|
||||
<input
|
||||
class="search-input"
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索好友昵称"
|
||||
placeholder="搜索好友或用户名"
|
||||
placeholder-style="color: #94A3B8"
|
||||
/>
|
||||
<view v-if="searchKeyword" class="search-clear" @tap="searchKeyword = ''">
|
||||
@@ -86,6 +86,39 @@
|
||||
<text class="empty-text">{{ searchKeyword ? '未找到匹配的好友' : '没有可邀请的好友' }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 搜索到的非好友用户 -->
|
||||
<view v-if="showSearchSection" class="section" style="margin-top: 16rpx;">
|
||||
<view class="section-header">
|
||||
<text class="section-title">搜索到的其他用户</text>
|
||||
</view>
|
||||
<view v-if="searching" class="loading-tip">
|
||||
<text class="loading-tip-text">搜索中...</text>
|
||||
</view>
|
||||
<view v-else-if="nonFriendResults.length > 0" class="friend-list">
|
||||
<view
|
||||
v-for="user in nonFriendResults"
|
||||
:key="user.id"
|
||||
class="friend-item"
|
||||
@tap="toggleSelect(user)"
|
||||
>
|
||||
<view class="checkbox" :class="{ 'checkbox--checked': isSelected(user.id) }">
|
||||
<text v-if="isSelected(user.id)" class="checkbox-icon">✓</text>
|
||||
</view>
|
||||
<view class="avatar" :style="{ backgroundColor: getAvatarColor(user.nickname || user.username) }">
|
||||
<text class="avatar-text">{{ getInitial(user.nickname || user.username) }}</text>
|
||||
</view>
|
||||
<view class="friend-info">
|
||||
<text class="friend-name">{{ user.nickname || user.username }}</text>
|
||||
<text class="friend-account">@{{ user.username }}</text>
|
||||
</view>
|
||||
<text class="non-friend-tag">非好友</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="empty-state">
|
||||
<text class="empty-text">未找到其他匹配用户</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部占位 -->
|
||||
<view class="bottom-spacer"></view>
|
||||
|
||||
@@ -105,10 +138,11 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { useContactStore } from '@/store/contact'
|
||||
import { useGroupStore } from '@/store/group'
|
||||
import userApi from '@/api/user'
|
||||
import { getAvatarColor, getInitial } from '@/utils/avatar'
|
||||
|
||||
export default {
|
||||
@@ -119,9 +153,12 @@ export default {
|
||||
|
||||
const groupId = ref(0)
|
||||
const searchKeyword = ref('')
|
||||
const selectedIds = ref({})
|
||||
const selectedMap = ref({})
|
||||
const loading = ref(true)
|
||||
const submitting = ref(false)
|
||||
const searchedUsers = ref([])
|
||||
const searching = ref(false)
|
||||
let searchTimer = null
|
||||
|
||||
/** 已在群内的成员 ID 集合 */
|
||||
const memberIdSet = computed(() => {
|
||||
@@ -132,6 +169,9 @@ export default {
|
||||
return set
|
||||
})
|
||||
|
||||
/** 好友 ID 集合 */
|
||||
const friendIdSet = computed(() => new Set(contactStore.friendList.map(f => f.user_id)))
|
||||
|
||||
/** 过滤掉已在群内的好友 + 搜索关键词过滤 */
|
||||
const availableFriends = computed(() => {
|
||||
const filtered = contactStore.friendList.filter(f => !memberIdSet.value.has(f.user_id))
|
||||
@@ -145,34 +185,69 @@ export default {
|
||||
})
|
||||
})
|
||||
|
||||
/** 已选中的好友列表 */
|
||||
const selectedList = computed(() => {
|
||||
return contactStore.friendList.filter(f => selectedIds.value[f.user_id])
|
||||
/** 搜索到的非好友、非群成员用户 */
|
||||
const nonFriendResults = computed(() => {
|
||||
return searchedUsers.value.filter(u => !friendIdSet.value.has(u.id) && !memberIdSet.value.has(u.id))
|
||||
})
|
||||
|
||||
/** 判断好友是否已选中 */
|
||||
/** 是否显示全站搜索区块 */
|
||||
const showSearchSection = computed(() => {
|
||||
return searchKeyword.value.trim().length > 0 && (nonFriendResults.value.length > 0 || searching.value)
|
||||
})
|
||||
|
||||
/** 已选中的用户列表 */
|
||||
const selectedList = computed(() => Object.values(selectedMap.value))
|
||||
|
||||
/** 判断用户是否已选中 */
|
||||
const isSelected = (userId) => {
|
||||
return !!selectedIds.value[userId]
|
||||
return !!selectedMap.value[userId]
|
||||
}
|
||||
|
||||
/** 切换好友选中状态 */
|
||||
/** 切换用户选中状态 */
|
||||
const toggleSelect = (friend) => {
|
||||
const newMap = { ...selectedIds.value }
|
||||
if (newMap[friend.user_id]) {
|
||||
delete newMap[friend.user_id]
|
||||
const newMap = { ...selectedMap.value }
|
||||
const uid = friend.user_id || friend.id
|
||||
if (newMap[uid]) {
|
||||
delete newMap[uid]
|
||||
} else {
|
||||
newMap[friend.user_id] = true
|
||||
newMap[uid] = {
|
||||
user_id: uid,
|
||||
nickname: friend.nickname || friend.username || '',
|
||||
username: friend.username || '',
|
||||
remark: friend.remark || '',
|
||||
avatar: friend.avatar || ''
|
||||
}
|
||||
}
|
||||
selectedIds.value = newMap
|
||||
selectedMap.value = newMap
|
||||
}
|
||||
|
||||
/** 搜索关键词变化时延迟搜索全站用户 */
|
||||
watch(searchKeyword, (val) => {
|
||||
if (searchTimer) clearTimeout(searchTimer)
|
||||
const kw = val.trim()
|
||||
if (kw.length < 2) {
|
||||
searchedUsers.value = []
|
||||
return
|
||||
}
|
||||
searchTimer = setTimeout(async () => {
|
||||
searching.value = true
|
||||
try {
|
||||
const res = await userApi.searchUsers(kw, 1, 20)
|
||||
searchedUsers.value = res.data?.list || []
|
||||
} catch {
|
||||
searchedUsers.value = []
|
||||
}
|
||||
searching.value = false
|
||||
}, 400)
|
||||
})
|
||||
|
||||
/** 提交邀请 */
|
||||
const handleInvite = async () => {
|
||||
if (selectedList.value.length === 0 || submitting.value) return
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
const userIds = selectedList.value.map(f => f.user_id)
|
||||
const userIds = selectedList.value.map(f => f.user_id || f.id)
|
||||
await groupStore.inviteMembers(groupId.value, userIds)
|
||||
|
||||
uni.showToast({ title: '邀请成功', icon: 'success' })
|
||||
@@ -183,7 +258,7 @@ export default {
|
||||
} catch (e) {
|
||||
console.error('邀请入群失败', e)
|
||||
uni.showToast({
|
||||
title: e?.data?.message || '邀请失败',
|
||||
title: e?.message || '邀请失败',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
@@ -212,6 +287,9 @@ export default {
|
||||
searchKeyword,
|
||||
selectedList,
|
||||
availableFriends,
|
||||
nonFriendResults,
|
||||
showSearchSection,
|
||||
searching,
|
||||
loading,
|
||||
submitting,
|
||||
isSelected,
|
||||
@@ -483,6 +561,25 @@ export default {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.non-friend-tag {
|
||||
color: #D97706;
|
||||
font-size: 22rpx;
|
||||
background-color: #FFFBEB;
|
||||
padding: 2rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.loading-tip {
|
||||
padding: 40rpx 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.loading-tip-text {
|
||||
font-size: 26rpx;
|
||||
color: #94A3B8;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
padding: 80rpx 0;
|
||||
|
||||
@@ -44,13 +44,13 @@
|
||||
<view
|
||||
v-else
|
||||
class="request-avatar request-avatar-placeholder"
|
||||
:style="{ backgroundColor: getAvatarColor(req.nickname || req.username) }"
|
||||
:style="{ backgroundColor: getAvatarColor(req.user_nickname || req.nickname) }"
|
||||
>
|
||||
<text class="avatar-char">{{ getInitial(req.nickname || req.username) }}</text>
|
||||
<text class="avatar-char">{{ getInitial(req.user_nickname || req.nickname) }}</text>
|
||||
</view>
|
||||
|
||||
<view class="request-info">
|
||||
<text class="request-name">{{ req.nickname || req.username }}</text>
|
||||
<text class="request-name">{{ req.user_nickname || req.nickname }}</text>
|
||||
<text class="request-msg" v-if="req.message">{{ req.message }}</text>
|
||||
<text class="request-msg" v-else>申请加入群聊</text>
|
||||
<text class="request-time">{{ formatTime(req.created_at) }}</text>
|
||||
@@ -96,13 +96,13 @@
|
||||
<view
|
||||
v-else
|
||||
class="request-avatar request-avatar-placeholder"
|
||||
:style="{ backgroundColor: getAvatarColor(req.nickname || req.username) }"
|
||||
:style="{ backgroundColor: getAvatarColor(req.user_nickname || req.nickname) }"
|
||||
>
|
||||
<text class="avatar-char">{{ getInitial(req.nickname || req.username) }}</text>
|
||||
<text class="avatar-char">{{ getInitial(req.user_nickname || req.nickname) }}</text>
|
||||
</view>
|
||||
|
||||
<view class="request-info">
|
||||
<text class="request-name">{{ req.nickname || req.username }}</text>
|
||||
<text class="request-name">{{ req.user_nickname || req.nickname }}</text>
|
||||
<text class="request-msg" v-if="req.message">{{ req.message }}</text>
|
||||
<text class="request-msg" v-else>申请加入群聊</text>
|
||||
<text class="request-time">{{ formatTime(req.created_at) }}</text>
|
||||
@@ -207,7 +207,7 @@ export default {
|
||||
await groupStore.fetchJoinRequests(groupId.value)
|
||||
} catch (e) {
|
||||
console.error('获取入群申请失败', e)
|
||||
uni.showToast({ title: e?.data?.message || '获取申请列表失败', icon: 'none' })
|
||||
uni.showToast({ title: e?.message || '获取申请列表失败', icon: 'none' })
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
@@ -224,7 +224,7 @@ export default {
|
||||
uni.showToast({ title: '已通过', icon: 'success' })
|
||||
} catch (e) {
|
||||
console.error('审批通过失败', e)
|
||||
uni.showToast({ title: e?.data?.message || '操作失败', icon: 'none' })
|
||||
uni.showToast({ title: e?.message || '操作失败', icon: 'none' })
|
||||
} finally {
|
||||
processingMap[req.id] = false
|
||||
}
|
||||
@@ -240,7 +240,7 @@ export default {
|
||||
uni.showToast({ title: '已拒绝', icon: 'none' })
|
||||
} catch (e) {
|
||||
console.error('审批拒绝失败', e)
|
||||
uni.showToast({ title: e?.data?.message || '操作失败', icon: 'none' })
|
||||
uni.showToast({ title: e?.message || '操作失败', icon: 'none' })
|
||||
} finally {
|
||||
processingMap[req.id] = false
|
||||
}
|
||||
|
||||
@@ -45,28 +45,30 @@
|
||||
<view
|
||||
v-else
|
||||
class="member-avatar member-avatar-placeholder"
|
||||
:style="{ backgroundColor: getAvatarColor(member.nickname || member.username) }"
|
||||
:style="{ backgroundColor: getAvatarColor(member.nickname || member.user_nickname) }"
|
||||
>
|
||||
<text class="avatar-char">{{ getInitial(member.nickname || member.username) }}</text>
|
||||
<text class="avatar-char">{{ getInitial(member.nickname || member.user_nickname) }}</text>
|
||||
</view>
|
||||
|
||||
<view class="member-info">
|
||||
<view class="member-name-row">
|
||||
<text class="member-name">{{ member.nickname || member.username }}</text>
|
||||
<text class="member-name">{{ member.nickname || member.user_nickname }}</text>
|
||||
<text v-if="member.role === GROUP_ROLE.OWNER" class="role-tag role-tag-owner">🔑 群主</text>
|
||||
<text v-else-if="member.role === GROUP_ROLE.ADMIN" class="role-tag role-tag-admin">🛡 管理员</text>
|
||||
<text v-else class="role-tag role-tag-member">👤 成员</text>
|
||||
</view>
|
||||
<view v-if="member.is_muted" class="muted-row">
|
||||
<text class="muted-tag">已禁言</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<uni-icons
|
||||
<view
|
||||
v-if="canManage(member)"
|
||||
type="more-filled"
|
||||
size="18"
|
||||
color="#94A3B8"
|
||||
/>
|
||||
class="manage-btn"
|
||||
@tap.stop="onMemberLongPress(member)"
|
||||
>
|
||||
<uni-icons type="more-filled" size="18" color="#94A3B8" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -78,6 +80,38 @@
|
||||
<view class="fab-btn" @tap="goToInvite">
|
||||
<uni-icons type="plusempty" size="24" color="#FFFFFF" />
|
||||
</view>
|
||||
|
||||
<!-- 自定义操作弹窗 -->
|
||||
<view v-if="actionTarget" class="action-overlay" @tap="actionTarget = null">
|
||||
<view class="action-sheet" @tap.stop>
|
||||
<view class="action-sheet-header">
|
||||
<view class="action-sheet-avatar" :style="{ backgroundColor: getAvatarColor(actionTarget.nickname || actionTarget.user_nickname) }">
|
||||
<text class="action-sheet-avatar-text">{{ getInitial(actionTarget.nickname || actionTarget.user_nickname) }}</text>
|
||||
</view>
|
||||
<view class="action-sheet-info">
|
||||
<text class="action-sheet-name">{{ actionTarget.nickname || actionTarget.user_nickname }}</text>
|
||||
<text class="action-sheet-role">{{ GROUP_ROLE_LABEL[actionTarget.role] || '成员' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="action-sheet-divider"></view>
|
||||
<view class="action-sheet-actions">
|
||||
<view
|
||||
v-for="action in actionList"
|
||||
:key="action.label"
|
||||
class="action-sheet-item"
|
||||
:class="{ 'action-sheet-item--danger': action.danger }"
|
||||
@tap="doAction(action.key)"
|
||||
>
|
||||
<text class="action-sheet-item-icon">{{ action.icon }}</text>
|
||||
<text class="action-sheet-item-label">{{ action.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="action-sheet-divider"></view>
|
||||
<view class="action-sheet-cancel" @tap="actionTarget = null">
|
||||
<text class="action-sheet-cancel-text">取消</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -87,7 +121,7 @@ import { ref, computed } from 'vue'
|
||||
import { useGroupStore } from '@/store/group'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { getAvatarColor, getInitial } from '@/utils/avatar'
|
||||
import { GROUP_ROLE } from '@/constants/group'
|
||||
import { GROUP_ROLE, GROUP_ROLE_LABEL } from '@/constants/group'
|
||||
|
||||
export default {
|
||||
name: 'GroupMembers',
|
||||
@@ -113,8 +147,8 @@ export default {
|
||||
if (!kw) return currentMembers.value
|
||||
return currentMembers.value.filter(m => {
|
||||
const nickname = (m.nickname || '').toLowerCase()
|
||||
const username = (m.username || '').toLowerCase()
|
||||
return nickname.includes(kw) || username.includes(kw)
|
||||
const userNickname = (m.user_nickname || '').toLowerCase()
|
||||
return nickname.includes(kw) || userNickname.includes(kw)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -143,51 +177,54 @@ export default {
|
||||
|
||||
// ==================== 管理操作 ====================
|
||||
|
||||
const onMemberLongPress = (member) => {
|
||||
if (!canManage(member)) return
|
||||
const actionTarget = ref(null)
|
||||
|
||||
const actions = []
|
||||
/** 构建当前操作目标的可用操作列表 */
|
||||
const actionList = computed(() => {
|
||||
const member = actionTarget.value
|
||||
if (!member) return []
|
||||
|
||||
const list = []
|
||||
if (myRole.value === GROUP_ROLE.OWNER) {
|
||||
if (member.role === GROUP_ROLE.ADMIN) {
|
||||
actions.push('取消管理员')
|
||||
list.push({ key: 'unsetAdmin', icon: '👤', label: '取消管理员' })
|
||||
} else {
|
||||
actions.push('设为管理员')
|
||||
list.push({ key: 'setAdmin', icon: '🛡', label: '设为管理员' })
|
||||
}
|
||||
}
|
||||
|
||||
if (member.is_muted) {
|
||||
actions.push('解除禁言')
|
||||
list.push({ key: 'unmute', icon: '🔊', label: '解除禁言' })
|
||||
} else {
|
||||
actions.push('禁言')
|
||||
list.push({ key: 'mute', icon: '🔇', label: '禁言' })
|
||||
}
|
||||
list.push({ key: 'kick', icon: '🚫', label: '踢出群聊', danger: true })
|
||||
return list
|
||||
})
|
||||
|
||||
actions.push('踢出群聊')
|
||||
|
||||
uni.showActionSheet({
|
||||
itemList: actions,
|
||||
success: (res) => {
|
||||
const action = actions[res.tapIndex]
|
||||
handleAction(action, member)
|
||||
}
|
||||
})
|
||||
const onMemberLongPress = (member) => {
|
||||
if (!canManage(member)) return
|
||||
actionTarget.value = member
|
||||
}
|
||||
|
||||
const handleAction = (action, member) => {
|
||||
switch (action) {
|
||||
case '设为管理员':
|
||||
const doAction = (key) => {
|
||||
const member = actionTarget.value
|
||||
if (!member) return
|
||||
actionTarget.value = null
|
||||
|
||||
switch (key) {
|
||||
case 'setAdmin':
|
||||
confirmSetRole(member, GROUP_ROLE.ADMIN)
|
||||
break
|
||||
case '取消管理员':
|
||||
case 'unsetAdmin':
|
||||
confirmSetRole(member, GROUP_ROLE.MEMBER)
|
||||
break
|
||||
case '禁言':
|
||||
case 'mute':
|
||||
confirmMute(member, true)
|
||||
break
|
||||
case '解除禁言':
|
||||
case 'unmute':
|
||||
confirmMute(member, false)
|
||||
break
|
||||
case '踢出群聊':
|
||||
case 'kick':
|
||||
confirmKick(member)
|
||||
break
|
||||
}
|
||||
@@ -195,7 +232,7 @@ export default {
|
||||
|
||||
const confirmSetRole = (member, role) => {
|
||||
const roleName = role === GROUP_ROLE.ADMIN ? '管理员' : '普通成员'
|
||||
const name = member.nickname || member.username
|
||||
const name = member.nickname || member.user_nickname
|
||||
uni.showModal({
|
||||
title: '确认操作',
|
||||
content: `确定将 ${name} 设为${roleName}吗?`,
|
||||
@@ -205,7 +242,7 @@ export default {
|
||||
await groupStore.setMemberRole(groupId.value, member.user_id, role)
|
||||
uni.showToast({ title: '操作成功', icon: 'success' })
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e?.data?.message || '操作失败', icon: 'none' })
|
||||
uni.showToast({ title: e?.message || '操作失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -213,7 +250,7 @@ export default {
|
||||
}
|
||||
|
||||
const confirmMute = (member, isMuted) => {
|
||||
const name = member.nickname || member.username
|
||||
const name = member.nickname || member.user_nickname
|
||||
const tip = isMuted ? `确定禁言 ${name} 吗?` : `确定解除 ${name} 的禁言吗?`
|
||||
uni.showModal({
|
||||
title: '确认操作',
|
||||
@@ -224,7 +261,7 @@ export default {
|
||||
await groupStore.muteMember(groupId.value, member.user_id, isMuted)
|
||||
uni.showToast({ title: '操作成功', icon: 'success' })
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e?.data?.message || '操作失败', icon: 'none' })
|
||||
uni.showToast({ title: e?.message || '操作失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -232,7 +269,7 @@ export default {
|
||||
}
|
||||
|
||||
const confirmKick = (member) => {
|
||||
const name = member.nickname || member.username
|
||||
const name = member.nickname || member.user_nickname
|
||||
uni.showModal({
|
||||
title: '踢出群聊',
|
||||
content: `确定将 ${name} 踢出群聊吗?`,
|
||||
@@ -243,7 +280,7 @@ export default {
|
||||
await groupStore.kickMember(groupId.value, member.user_id)
|
||||
uni.showToast({ title: '已踢出', icon: 'success' })
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e?.data?.message || '操作失败', icon: 'none' })
|
||||
uni.showToast({ title: e?.message || '操作失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -260,10 +297,14 @@ export default {
|
||||
|
||||
return {
|
||||
GROUP_ROLE,
|
||||
GROUP_ROLE_LABEL,
|
||||
searchKeyword,
|
||||
filteredMembers,
|
||||
canManage,
|
||||
onMemberLongPress,
|
||||
actionTarget,
|
||||
actionList,
|
||||
doAction,
|
||||
goToInvite,
|
||||
getAvatarColor,
|
||||
getInitial
|
||||
@@ -404,6 +445,11 @@ export default {
|
||||
background-color: rgba(37, 99, 235, 0.08);
|
||||
}
|
||||
|
||||
.role-tag-member {
|
||||
color: #64748B;
|
||||
background-color: rgba(100, 116, 139, 0.08);
|
||||
}
|
||||
|
||||
/* ===== 禁言标签 ===== */
|
||||
.muted-row {
|
||||
display: flex;
|
||||
@@ -418,6 +464,23 @@ export default {
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
|
||||
/* ===== 管理按钮 ===== */
|
||||
.manage-btn {
|
||||
flex-shrink: 0;
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 12rpx;
|
||||
cursor: pointer;
|
||||
transition: background-color 150ms ease;
|
||||
}
|
||||
|
||||
.manage-btn:active {
|
||||
background-color: #F1F5F9;
|
||||
}
|
||||
|
||||
/* ===== 空状态 ===== */
|
||||
.empty-state {
|
||||
padding: 80rpx 0;
|
||||
@@ -450,4 +513,139 @@ export default {
|
||||
.fab-btn:active {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
/* ===== 自定义操作弹窗 ===== */
|
||||
.action-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 999;
|
||||
animation: fadeIn 200ms ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.action-sheet {
|
||||
width: 560rpx;
|
||||
max-width: 90vw;
|
||||
background-color: #FFFFFF;
|
||||
border-radius: 24rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.15);
|
||||
animation: slideUp 200ms ease;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from { transform: translateY(40rpx); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
|
||||
.action-sheet-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 28rpx 32rpx;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.action-sheet-avatar {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 18rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.action-sheet-avatar-text {
|
||||
font-size: 28rpx;
|
||||
color: #FFFFFF;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.action-sheet-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
.action-sheet-name {
|
||||
font-size: 30rpx;
|
||||
color: #1E293B;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.action-sheet-role {
|
||||
font-size: 24rpx;
|
||||
color: #94A3B8;
|
||||
}
|
||||
|
||||
.action-sheet-divider {
|
||||
height: 1rpx;
|
||||
background-color: #F1F5F9;
|
||||
margin: 0 24rpx;
|
||||
}
|
||||
|
||||
.action-sheet-actions {
|
||||
padding: 8rpx 0;
|
||||
}
|
||||
|
||||
.action-sheet-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 24rpx 32rpx;
|
||||
gap: 16rpx;
|
||||
cursor: pointer;
|
||||
transition: background-color 150ms ease;
|
||||
}
|
||||
|
||||
.action-sheet-item:active {
|
||||
background-color: #F8FAFC;
|
||||
}
|
||||
|
||||
.action-sheet-item-icon {
|
||||
font-size: 32rpx;
|
||||
width: 40rpx;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.action-sheet-item-label {
|
||||
font-size: 30rpx;
|
||||
color: #334155;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.action-sheet-item--danger .action-sheet-item-label {
|
||||
color: #EF4444;
|
||||
}
|
||||
|
||||
.action-sheet-cancel {
|
||||
padding: 24rpx 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: background-color 150ms ease;
|
||||
}
|
||||
|
||||
.action-sheet-cancel:active {
|
||||
background-color: #F8FAFC;
|
||||
}
|
||||
|
||||
.action-sheet-cancel-text {
|
||||
font-size: 30rpx;
|
||||
color: #94A3B8;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -72,7 +72,11 @@
|
||||
<text v-if="group.description" class="group-desc">{{ group.description }}</text>
|
||||
</view>
|
||||
|
||||
<view v-if="isInGroup(group.id)" class="action-btn action-btn--joined">
|
||||
<text class="action-btn-text action-btn-text--joined">已加入</text>
|
||||
</view>
|
||||
<view
|
||||
v-else
|
||||
class="action-btn action-btn--apply"
|
||||
:class="{ 'action-btn--disabled': appliedMap[group.id] }"
|
||||
@tap="showApplyDialog(group)"
|
||||
@@ -110,12 +114,28 @@
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { ref, computed, reactive } from 'vue'
|
||||
import { useGroupStore } from '@/store/group'
|
||||
import { useChatStore } from '@/store/chat'
|
||||
import { getAvatarColor, getInitial } from '@/utils/avatar'
|
||||
|
||||
export default {
|
||||
name: 'GroupSearch',
|
||||
setup() {
|
||||
const groupStore = useGroupStore()
|
||||
const chatStore = useChatStore()
|
||||
|
||||
/** 当前用户已加入的群 ID 集合 */
|
||||
const myGroupIds = computed(() => {
|
||||
const ids = new Set()
|
||||
chatStore.conversationList.forEach(conv => {
|
||||
if (conv.type === 2 && conv.group_id) {
|
||||
ids.add(conv.group_id)
|
||||
}
|
||||
})
|
||||
return ids
|
||||
})
|
||||
|
||||
/** 判断是否已在该群内 */
|
||||
const isInGroup = (groupId) => myGroupIds.value.has(groupId)
|
||||
|
||||
const keyword = ref('')
|
||||
const searched = ref(false)
|
||||
@@ -138,6 +158,9 @@ export default {
|
||||
onLoad(() => {
|
||||
groupStore.searchResults = []
|
||||
groupStore.searchTotal = 0
|
||||
if (chatStore.conversationList.length === 0) {
|
||||
chatStore.fetchConversations()
|
||||
}
|
||||
})
|
||||
|
||||
// ==================== 搜索逻辑 ====================
|
||||
@@ -158,7 +181,7 @@ export default {
|
||||
await groupStore.searchGroups(kw, 1, pageSize)
|
||||
} catch (e) {
|
||||
console.error('搜索群聊失败', e)
|
||||
uni.showToast({ title: e?.data?.message || '搜索失败', icon: 'none' })
|
||||
uni.showToast({ title: e?.message || '搜索失败', icon: 'none' })
|
||||
}
|
||||
|
||||
searchLoading.value = false
|
||||
@@ -200,7 +223,7 @@ export default {
|
||||
uni.showToast({ title: '申请已发送', icon: 'success' })
|
||||
} catch (e) {
|
||||
console.error('发送入群申请失败', e)
|
||||
uni.showToast({ title: e?.data?.message || '申请失败', icon: 'none' })
|
||||
uni.showToast({ title: e?.message || '申请失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -216,6 +239,7 @@ export default {
|
||||
resultList,
|
||||
noMore,
|
||||
appliedMap,
|
||||
isInGroup,
|
||||
doSearch,
|
||||
loadMore,
|
||||
showApplyDialog,
|
||||
@@ -421,6 +445,18 @@ export default {
|
||||
color: #94A3B8;
|
||||
}
|
||||
|
||||
.action-btn--joined {
|
||||
border-color: #059669;
|
||||
background-color: #ECFDF5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.action-btn-text--joined {
|
||||
color: #059669;
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ===== 加载更多 ===== */
|
||||
.load-more-wrap {
|
||||
padding: 24rpx 0 40rpx;
|
||||
|
||||
@@ -58,11 +58,11 @@
|
||||
<view
|
||||
v-else
|
||||
class="member-preview-avatar member-preview-avatar-placeholder"
|
||||
:style="{ backgroundColor: getAvatarColor(member.nickname || member.username) }"
|
||||
:style="{ backgroundColor: getAvatarColor(member.nickname || member.user_nickname) }"
|
||||
>
|
||||
<text class="member-preview-char">{{ getInitial(member.nickname || member.username) }}</text>
|
||||
<text class="member-preview-char">{{ getInitial(member.nickname || member.user_nickname) }}</text>
|
||||
</view>
|
||||
<text class="member-preview-name">{{ member.nickname || member.username }}</text>
|
||||
<text class="member-preview-name">{{ member.nickname || member.user_nickname }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
@@ -192,7 +192,7 @@ export default {
|
||||
groupName.value = res.content.trim()
|
||||
uni.showToast({ title: '修改成功', icon: 'success' })
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e?.data?.message || '修改失败', icon: 'none' })
|
||||
uni.showToast({ title: e?.message || '修改失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -214,7 +214,7 @@ export default {
|
||||
await groupStore.updateGroup(groupId.value, { notice: res.content || '' })
|
||||
uni.showToast({ title: '修改成功', icon: 'success' })
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e?.data?.message || '修改失败', icon: 'none' })
|
||||
uni.showToast({ title: e?.message || '修改失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -243,7 +243,7 @@ export default {
|
||||
groupStore.fetchMembers(groupId.value)
|
||||
uni.showToast({ title: '修改成功', icon: 'success' })
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e?.data?.message || '修改失败', icon: 'none' })
|
||||
uni.showToast({ title: e?.message || '修改失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -279,7 +279,7 @@ export default {
|
||||
uni.switchTab({ url: '/pages/chat/index' })
|
||||
}, 500)
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e?.data?.message || '解散失败', icon: 'none' })
|
||||
uni.showToast({ title: e?.message || '解散失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -302,7 +302,7 @@ export default {
|
||||
uni.switchTab({ url: '/pages/chat/index' })
|
||||
}, 500)
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e?.data?.message || '退出失败', icon: 'none' })
|
||||
uni.showToast({ title: e?.message || '退出失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user