这个是CafeGroupDetails function enterSubTeam(item) { ...
创建于:2025年12月26日
使用 GPT-5.2 Thinking 回答 Chat01
创建于:2025年12月26日
使用 GPT-5.2 Thinking 回答 Chat01
这个是CafeGroupDetails function enterSubTeam(item) {
console.log(item,'里面的参数')
if (!item) return
uni.navigateTo({
url: '/pages/sociAlize/Coffeegroup/Groupchat/Groupchat' +
'?from=subTeam' +
'&team=' + encodeURIComponent(JSON.stringify(item))
})
}
然后这个item里面的参数avatarFileArray: (2) ["images/avatar/634f7bd4-ff03-47be-9451-10d6e3209b0e.png", "images/avatar/11e38d68-7fb6-4e13-9834-7b5292307640.jpg"]
castingTeamId: "d0324a59-9939-4a59-92fa-f580a9a387d2"
castingTeamJoinlock: true
castingTeamLock: false
castingTeamMute: false
castingTeamTitle: "测试组"
count: 2
doNotDisturb: false
manager: false
nickname: "你好.幻影"
owner: true
parentId: "2671984d-9686-4ad4-b198-da44b55770d5"
top: false
点击大组和小组都调到这个页面 uni.navigateTo({
url:'/pages/sociAlize/Coffeegroup/Groupchat/Groupchat'
}) 下面是这个页面的内容Groupchat <!-- pages/emitsgroupchat/groupChat.vue -->
<template>
<view :class="['app-container', theme]">
<view class="content">
<view class="overlay">
<!-- Header -->
<view class="header" :style="{ paddingTop: statusBarHeight + 'px', height: headerBarHeight + 'px' }">
<view class="header-inner">
<view class="left-box" @click="navigateBack">
<uni-icons type="left" color="#000000" size="26"></uni-icons>
</view>
<view class="center-box">
<text @click="details" class="title-text">{{ displayGroupTitle }}</text>
</view>
<view class="right-box">
<!-- ✅ 右上角更多:群设置 / 清空聊天 -->
<text @click="openMoreMenu" class="desc-text">...</text>
</view>
</view>
</view>
</template> <script setup> import { ref, reactive, toRefs, nextTick, onMounted, onUnmounted, shallowRef, computed } from 'vue' import { onLoad, onShow } from '@dcloudio/uni-app' import { storeToRefs } from 'pinia' import ChatBubble from '@/components/ChatBubble.vue' import WsClient from '@/utils/WsClient.js' import ajax from '@/api/ajax.js' import { useMainStore } from '@/store/index.js' import { usecontacts } from '@/store/usecontacts.js' import { useGroupsettings } from '@/store/useGroupsetup.js' import { getStatusBarHeight, getTitleBarHeight } from '@/utils/system.js' // ✅ 新增:群聊清空本地记录工具(按 consumerId + groupId 精准清除) import { clearLocalGroupChatHistory, getGroupClearedAtISO } from '@/utils/system.js' const API_BASE = (getApp().globalData && getApp().globalData.serverUrl) || 'http://47.99.182.213:7018/applet/' const uploadUrlGroup = `${API_BASE.replace(/\/$/, '')}/castingChatGroupChatFile` // ✅ 打码头像固定替换 const MASK_AVATAR_URL = 'https://img1.baidu.com/it/u=4080682647,2563443672&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500' function maskText(str = '') { const s = String(str || '') const n = Array.from(s).length return n ? '*'.repeat(n) : '*' } // 主题 const store = useMainStore() const { theme } = storeToRefs(store) // 其它 store const storects = usecontacts() const { Getgroupchatmessages } = storects const { GroupDetails, SelectgroupID, Creategroupnickname } = storeToRefs(storects) const { Groupfriends } = storeToRefs(useGroupsettings()) // 顶部高度 const statusBarHeight = ref(getStatusBarHeight()) const headerBarHeight = ref(getTitleBarHeight()) // 导航 const navigateBack = () => uni.navigateBack() const details = () => { let option = { RemoveCoffee: RemoveCoffee, SelectgroupID: SelectgroupID } uni.navigateTo({ url: '/pages/emitsgroupchat/Groupsettings/Groupsettings?GroupID=' + encodeURIComponent(JSON .stringify(option)) }) } // ✅ 右上角更多:群设置/清空聊天 function openMoreMenu() { uni.showActionSheet({ itemList: ['群设置', '清空聊天记录(仅本地)'], success: (res) => { if (res.tapIndex === 0) { details() } else if (res.tapIndex === 1) { clearCurrentGroupLocalHistory() } } }) } // ✅ 是否打码群 const isMaskedGroup = ref(false) // 数据源 const datasource = reactive({ containerTop: 0, inputAreaHeight: 0, safeAreaBottom: 0, lastItemId: '', inputText: '', messages: [], userAvatar: uni.getStorageSync('avatarUrl') || '', assistantAvatar: '/static/default/avatar.png' }) const { containerTop, inputAreaHeight, safeAreaBottom, lastItemId, inputText, messages, userAvatar, assistantAvatar } = toRefs(datasource) const Groupnickname = ref('') const invite = ref(false) const consumerId = ref(uni.getStorageSync('consumerId') || '') const groupId = ref('') const Creategroupid = ref('') const controltime = ref(true) const currentGroupId = computed(() => String(invite.value ? (Creategroupid.value || '') : (groupId.value || ''))) // ✅ 新增:本地清空时间(只展示 clearedAt 之后的消息) const groupClearedAtISO = ref('') const WS_HOST = ref('ws://47.99.182.213:7018/ws') const socket = shallowRef(null) const lastSendContent = ref('') const lastSendAt = ref(0) // 群权限:mute / screenshot / spread / copy / bookmark + 群主 / 管理员列表 const groupGuard = reactive({ mute: false, screenshot: false, spread: false, copy: false, bookmark: false }) const groupOwnerId = ref('') const managerIds = ref([]) // 计算:当前用户是否有发送权限 const isGroupOwner = computed(() => String(consumerId.value || '') === String(groupOwnerId.value || '')) const isGroupManager = computed(() => { const id = String(consumerId.value || '') return Array.isArray(managerIds.value) ? managerIds.value.some(mid => String(mid) === id) : false }) const canSendWhenMuted = computed(() => isGroupOwner.value || isGroupManager.value) const hasAnyGuard = computed(() => groupGuard.mute || groupGuard.screenshot || groupGuard.spread || groupGuard.copy || groupGuard.bookmark ) // 附件弹层 const attachShow = ref(false) function openAttachPanel() { attachShow.value = true } // ✅ 多选模式 const multiSelectMode = ref(false) const selectedIds = ref([]) // string[] const selectedCount = computed(() => selectedIds.value.length) // 群多选转发 & 收藏弹窗 const multiForwardPickerShow = ref(false) const multiCollectPickerShow = ref(false) const bottomBarHeight = computed(() => { if (multiSelectMode.value) return uni.upx2px(120) + safeAreaBottom.value return uni.upx2px(85) + safeAreaBottom.value }) function isSelected(id) { if (!id) return false return selectedIds.value.includes(String(id)) } function toggleSelect(msg) { const id = String(msg?.castingChatId || '') if (!id) return const arr = [...selectedIds.value] const idx = arr.indexOf(id) if (idx >= 0) arr.splice(idx, 1) else arr.push(id) selectedIds.value = arr } function exitMultiSelect() { multiSelectMode.value = false selectedIds.value = [] multiForwardPickerShow.value = false multiCollectPickerShow.value = false } // 工具函数:文件名 function filenameOf(path = '') { try { return String(path).split('/').pop() || '' } catch { return '' } } function buildUrl(base, url = '') { if (!url) return '' if (/^https?:\/\//i.test(url)) return url const b = String(base || '').replace(/\/$/, '') const p = String(url).replace(/^\//, '') return b ? `${b}/${p}` : p } // ✅ 头像补全(给消息用) function fullAvatarUrl(url = '') { if (!url) return '' if (/^https?:\/\//i.test(url)) return url const base = getApp().globalData?.webProjectUrl || '' return buildUrl(base, url) } function packFileContent({ type, playURL, showName, fileName, text }) { return JSON.stringify({ _kind: 'file', type, playURL, showName, fileName, text }) } // ✅ 新增:位置消息 payload(对齐单聊 Bubblechatbox 的 msgType=location) function packLocationContent(loc) { const clientId = 'loc_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 8) return JSON.stringify({ msgType: 'location', name: loc?.name || '位置', address: loc?.address || '', latitude: Number(loc?.latitude || 0), longitude: Number(loc?.longitude || 0), clientId, ts: Date.now() }) } // 群里单条/历史消息内容归一 function normalizeIncomingItem(item, webBase) { // forward/合并转发 if (item.type == 5 || item.type == 6) { try { return typeof item === 'string' ? item : JSON.stringify(item) } catch (e) { return String(item || '') } } // 1-4 文件 if (typeof item.type === 'number' && item.type > 0 && item.type <= 4) { return packFileContent({ type: item.type, playURL: buildUrl(webBase, item.playURL), showName: item.showName || item.fileName || '', fileName: item.fileName || '', text: item.content || '' }) } // 0 文本 / 其它:可能就是 json 字符串(location / text / forward 等) return typeof item.content === 'string' ? item.content : JSON.stringify(item.content) } // 时间显示逻辑 function shouldShowTimestamp(index) { if (index === 0) return true const cur = messages.value[index] const prev = messages.value[index - 1] if (!cur?.timestamp || !prev?.timestamp) return true const diff = Math.abs(cur.timestamp - prev.timestamp) return diff > 5 * 60 * 1000 } function formatChatTime(ts) { if (!ts) return '' const now = new Date() const msgTime = new Date(ts) const Yn = now.getFullYear() const Ym = msgTime.getFullYear() const mm = String(msgTime.getMonth() + 1).padStart(2, '0') const dd = String(msgTime.getDate()).padStart(2, '0') const hh = String(msgTime.getHours()).padStart(2, '0') const min = String(msgTime.getMinutes()).padStart(2, '0') const diffMs = now - msgTime const oneDay = 24 * 60 * 60 * 1000 if (Yn !== Ym) return `${Ym}年${mm}月${dd}日 ${hh}:${min}` if (diffMs > oneDay) return `${mm}月${dd}日 ${hh}:${min}` return `今天 ${hh}:${min}` } function scrollToBottom() { lastItemId.value = 'msg-' + (messages.value.length - 1) nextTick(() => { lastItemId.value = '' }) } // push 消息(新增 avatarUrl) function pushMsg({ castingChatId = '', content, senderId, createTime, nickname, avatarUrl }) { const ct = createTime || new Date().toISOString() const msg = { _localKey: `${castingChatId || 'tmp'}_${Date.now()}_${Math.random()}`, castingChatId, role: senderId === consumerId.value ? 'user' : 'assistant', content, timestamp: ct ? new Date(ct).getTime() : Date.now(), createTime: ct, senderId, nickname: nickname || '', avatarUrl: avatarUrl || '' } messages.value.push(msg) return msg } // 去重 function shouldDropAsDuplicate(incoming) { if (!incoming) return false if (incoming.senderId !== consumerId.value) return false if (!incoming.content) return false const now = Date.now() return incoming.content === lastSendContent.value && now - lastSendAt.value <= 5000 } // WebSocket function openSocket() { if (!consumerId.value) { uni.showToast({ title: '未登录,请先登录', icon: 'none' }) return } const url = `${WS_HOST.value}/${consumerId.value}` socket.value = new WsClient(url, { heartbeat: true, heartbeatInterval: 10000 }) socket.value.onOpen(() => {}) socket.value.getMessage(onWSMessage) socket.value.onError(() => {}) } function onWSMessage(raw) { let data if (typeof raw === 'string') { try { data = JSON.parse(raw) } catch (e) { return } } else if (raw && typeof raw === 'object') data = raw else return const list = Array.isArray(data) ? data : [data] list.forEach(item => { if (!item || typeof item !== 'object') return if (!('senderId' in item)) return const normalizedContent = normalizeIncomingItem(item, getApp().globalData?.webProjectUrl) const patched = { ...item, content: normalizedContent } if (shouldDropAsDuplicate(patched)) return // ✅ 如果本地已清空:丢弃 clearedAt 之前的消息(防止 ws 推旧消息) if (groupClearedAtISO.value) { const t0 = new Date(groupClearedAtISO.value).getTime() const t1 = item.createTime ? new Date(item.createTime).getTime() : 0 if (t0 && t1 && t1 <= t0) return } pushMsg({ castingChatId: item.id || '', content: normalizedContent, senderId: item.senderId, createTime: item.createTime, nickname: item.nickname || item.senderNickname || '', avatarUrl: fullAvatarUrl(item.avatarUrl || item.senderAvatarUrl || item.avatar || '') }) }) scrollToBottom() } // 发送文本 async function onSend() { controltime.value = false const text = (inputText.value || '').trim() if (!text) return uni.showToast({ title: '消息为空', icon: 'none' }) if (!consumerId.value) return uni.showToast({ title: '未登录', icon: 'none' }) if (groupGuard.mute && !canSendWhenMuted.value) { uni.showToast({ title: '全员禁言中,仅群主和管理员可发言', icon: 'none' }) return } const targetGroupId = invite.value ? Creategroupid.value : groupId.value if (!targetGroupId) return uni.showToast({ title: '群信息缺失', icon: 'none' }) lastSendContent.value = text lastSendAt.value = Date.now() const localMsg = pushMsg({ castingChatId: '', content: text, senderId: consumerId.value, createTime: new Date().toISOString(), nickname: parame.value?.nickname || '', avatarUrl: '' // 自己头像走 userAvatar }) scrollToBottom() inputText.value = '' try { const res = await ajax.post({ url: 'castingChatGroupChat', data: { senderId: consumerId.value, content: text, castingChatGroupId: targetGroupId } }) const csId = res?.data?.id if (localMsg && csId) localMsg.castingChatId = csId socket.value?.send(JSON.stringify({ type: 'castingChatGroupChat', id: csId })) } catch (e) { uni.showToast({ title: '网络错误了', icon: 'none' }) } } // ✅ 新增:发送位置到群(对齐单聊 sendLocationMessage 思路) async function sendLocationMessage(loc) { const targetGroupId = invite.value ? Creategroupid.value : groupId.value if (!targetGroupId) return uni.showToast({ title: '群信息缺失', icon: 'none' }) if (groupGuard.mute && !canSendWhenMuted.value) { uni.showToast({ title: '全员禁言中,仅群主和管理员可发言', icon: 'none' }) return } const content = packLocationContent(loc) lastSendContent.value = content lastSendAt.value = Date.now() const localMsg = pushMsg({ castingChatId: '', content, senderId: consumerId.value, createTime: new Date().toISOString(), nickname: parame.value?.nickname || '', avatarUrl: '' }) scrollToBottom() try { const res = await ajax.post({ url: 'castingChatGroupChat', data: { senderId: consumerId.value, content, castingChatGroupId: targetGroupId } }) const newId = res?.data?.id if (localMsg && newId) localMsg.castingChatId = newId socket.value?.send(JSON.stringify({ type: 'castingChatGroupChat', id: newId })) } catch (e) { uni.showToast({ title: '发送失败', icon: 'none' }) } } // ✅ 新增:定位入口(对齐单聊 sharedspace + eventChannel) function openLocation() { attachShow.value = false uni.navigateTo({ url: '/pages/sociAlize/sharedspace/sharedspace?chatConsumerId=' + encodeURIComponent(currentGroupId.value || ''), events: { sendLocation: async (loc) => { await sendLocationMessage(loc) } } }) } // ✅ 新增:收藏分享 / 文件分享入口(对齐单聊 collect + eventChannel) function openCollectShare(isFile = false) { attachShow.value = false const p = isFile ? '文件分享' : '收藏分享' uni.navigateTo({ url: '/pages/sociAlize/Coffee/Contact/collect?parameter=' + encodeURIComponent(p) + '&chatConsumerId=' + encodeURIComponent(currentGroupId.value || ''), events: { shareSelected: async (payload) => { const content = payload?.content || '' await sendSharedContentToCurrentChat(content) } } }) } // ✅ 新增:把 collect 选中的 content 发到当前群(对齐单聊 sendSharedContentToCurrentChat) async function sendSharedContentToCurrentChat(content) { const targetGroupId = invite.value ? Creategroupid.value : groupId.value if (!targetGroupId) return uni.showToast({ title: '群信息缺失', icon: 'none' }) if (!content) return if (groupGuard.mute && !canSendWhenMuted.value) { uni.showToast({ title: '全员禁言中,仅群主和管理员可发言', icon: 'none' }) return } lastSendContent.value = content lastSendAt.value = Date.now() const localMsg = pushMsg({ castingChatId: '', content, senderId: consumerId.value, createTime: new Date().toISOString(), nickname: parame.value?.nickname || '', avatarUrl: '' }) scrollToBottom() try { const res = await ajax.post({ url: 'castingChatGroupChat', data: { senderId: consumerId.value, content, castingChatGroupId: targetGroupId } }) const newId = res?.data?.id if (localMsg && newId) localMsg.castingChatId = newId socket.value?.send(JSON.stringify({ type: 'castingChatGroupChat', id: newId })) } catch (e) { uni.showToast({ title: '发送失败', icon: 'none' }) } } // 视频 async function Selectvideomedia() { attachShow.value = false uni.chooseVideo({ sourceType: ['album', 'camera'], maxDuration: 60, success: async (res) => { const filePath = res.tempFilePath if (!filePath) return await uploadAndSendGroup({ filePath, type: 2, showName: filenameOf(filePath), text: '' }) }, fail: (err) => { console.error('选择视频失败:', err) } }) } // ✅ 保留你原逻辑(里面引用到 Isfiletransfer/agent/Didpopup 等:不删不改) async function OpencameraImage() { if (Isfiletransfer.value) { Didpopup.value = false return uni.showToast({ title: '文件助手不支持发送图片', icon: 'none' }) } if (!agent.value) { Didpopup.value = false return uni.showToast({ title: '化身模式暂不支持发送图片', icon: 'none' }) } Didpopup.value = false uni.chooseImage({ count: 1, sizeType: ['original', 'compressed'], sourceType: ['camera'], success: async (res) => { const filePath = res.tempFilePaths?.[0] if (filePath) { await uploadAndSendSingle({ filePath, type: 1, showName: filenameOf(filePath), text: '' }) } } }) } // 图片 function pickAndSendImage() { attachShow.value = false uni.chooseImage({ count: 1, sizeType: ['original', 'compressed'], sourceType: ['album', 'camera'], success: async (res) => { const filePath = res.tempFilePaths?.[0] if (!filePath) return await uploadAndSendGroup({ filePath, type: 1, showName: filenameOf(filePath), text: '' }) } }) } // 文件 function pickAndSendFile() { attachShow.value = false // #ifdef MP-WEIXIN const choose = uni.chooseMessageFile || uni.chooseFile if (!choose) return uni.showToast({ title: '当前平台不支持选择文件', icon: 'none' }) choose({ count: 1, type: 'file', success: async (res) => { const f = (res.tempFiles && res.tempFiles[0]) || (res.tempFilePaths && { path: res.tempFilePaths[0] }) const filePath = f?.path || f?.tempFilePath if (!filePath) return await uploadAndSendGroup({ filePath, type: 4, showName: f?.name || filenameOf(filePath), text: '' }) } }) // #endif // #ifdef APP-PLUS if (uni.getSystemInfoSync().platform === 'android' || uni.getSystemInfoSync().platform === 'ios') { plus.io.chooseFile({ title: '选择文件', filter: '*', multiple: false }, async (res) => { const fileInfo = res.files[0] if (!fileInfo) return uni.showToast({ title: '选择错误', icon: 'none' }) await uploadAndSendGroup({ filePath: fileInfo, type: 4, showName: filenameOf(fileInfo), text: '' }) }, (err) => { console.error('选择文件失败:', err) } ) } // #endif } // 语音 const recorder = uni.getRecorderManager ? uni.getRecorderManager() : null function recordAndSendVoice() { attachShow.value = false if (!recorder) { uni.showToast({ title: '当前平台不支持录音', icon: 'none' }) return } uni.showActionSheet({ itemList: ['开始录音(最长60秒)'], success: () => { recorder.start({ duration: 60000, format: 'mp3' }) uni.showToast({ title: '录音中…再次点击结束', icon: 'none' }) recorder.onStart(() => { uni.showActionSheet({ itemList: ['结束并发送'], success: () => recorder.stop() }) }) recorder.onStop(async (res) => { const filePath = res.tempFilePath if (!filePath) return await uploadAndSendGroup({ filePath, type: 3, showName: filenameOf(filePath) || '语音', text: '' }) }) } }) } // 上传并发送群文件(图片/视频/语音/附件) async function uploadAndSendGroup({ filePath, type, showName, text }) { const targetGroupId = invite.value ? Creategroupid.value : groupId.value if (!targetGroupId) return uni.showToast({ title: '群信息缺失', icon: 'none' }) if (groupGuard.mute && !canSendWhenMuted.value) { uni.showToast({ title: '全员禁言中,仅群主和管理员可发言', icon: 'none' }) return } uni.showLoading({ title: '上传中...' }) const option = { castingChatGroupId: targetGroupId, senderId: consumerId.value, content: text || '', type, showName: showName || '' } try { const uploadRes = await uni.uploadFile({ url: uploadUrlGroup, filePath, name: 'chatFile', formData: option }) let data = {} try { data = typeof uploadRes.data === 'string' ? JSON.parse(uploadRes.data) : uploadRes.data } catch {} const newId = data?.id const playURL = data?.playURL || '' const normalized = packFileContent({ type, playURL: buildUrl(getApp().globalData?.webProjectUrl, playURL), showName, fileName: showName, text }) pushMsg({ castingChatId: newId || '', content: normalized, senderId: consumerId.value, createTime: new Date().toISOString(), nickname: parame.value?.nickname || '' }) scrollToBottom() socket.value?.send(JSON.stringify({ type: 'castingChatGroupChat', id: newId || 'tmp_' + Date.now() })) } catch (e) { uni.showToast({ title: '上传失败', icon: 'none' }) } finally { uni.hideLoading() } } // 拉取历史消息(✅ 支持 clearedAt:只展示清空时间之后) async function fetchHistory() { try { const id = invite.value ? Creategroupid.value : groupId.value if (!id) return // ✅ 读取本地清空时间(consumerId + groupId) groupClearedAtISO.value = getGroupClearedAtISO(consumerId.value, id) || '' const reqData = { castingChatGroupId: id, rowStartIdx: 0, rowCount: 50 } if (groupClearedAtISO.value) { reqData.time = groupClearedAtISO.value } const res = await ajax.post({ url: 'castingChatGroupChatList', data: reqData }) const clearedAtMs = groupClearedAtISO.value ? new Date(groupClearedAtISO.value).getTime() : 0 const arr = (res?.data?.castingChatGroupChatArray || []).map(m => { const normalized = normalizeIncomingItem(m, getApp().globalData?.webProjectUrl) const ct = m.createTime || new Date().toISOString() return { _localKey: `${m.id || 'h'}_${ct}_${Math.random()}`, castingChatId: m.id || '', role: m.senderId === consumerId.value ? 'user' : 'assistant', content: normalized, timestamp: ct ? new Date(ct).getTime() : Date.now(), createTime: ct, senderId: m.senderId, nickname: m.nickname || m.senderNickname || '', avatarUrl: fullAvatarUrl(m.avatarUrl || m.senderAvatarUrl || m.avatar || '') } }).filter(x => { if (!clearedAtMs) return true return Number(x.timestamp || 0) > clearedAtMs }) messages.value = arr scrollToBottom() } catch (e) { console.log('拉取历史失败:', e) } } // 群详情 const Groupname = ref('') const parame = ref({ nickname: '', ownerNickname: '' }) const parameter = ref({}) // ✅ 显示用群名:打码群按字数替换 * const displayGroupTitle = computed(() => { const raw = Groupname.value || '群聊' return isMaskedGroup.value ? maskText(raw) : raw }) // 拉取群名称 + 群权限信息 const getGroupname = (id) => { parameter.value = { castingChatGroupId: id || SelectgroupID.value, consumerId: uni.getStorageSync('consumerId') } return ajax.post({ url: 'castingChatGroupShow', data: parameter.value, methon: 'post' }) .then((res) => { if (res?.data?.result === 'success') { Groupname.value = res.data.title parame.value.nickname = res.data.nickname parame.value.ownerNickname = res.data.ownerNickname groupOwnerId.value = res.data.ownerId || '' managerIds.value = Array.isArray(res.data.managerArray) ? res.data.managerArray.map(m => String(m.consumerId)) : [] groupGuard.mute = !!res.data.mute groupGuard.screenshot = !!res.data.screenshot groupGuard.spread = !!res.data.spread groupGuard.copy = !!res.data.copy groupGuard.bookmark = !!res.data.bookmark } }) .catch(() => { uni.showToast({ icon: 'none' }) }) } // 截图监听(APP / 小程序) let appScreenshotListener = null onMounted(() => { Groupnickname.value = GroupDetails.value?.title || '' inputAreaHeight.value = uni.upx2px(120) const sys = uni.getSystemInfoSync() safeAreaBottom.value = sys.safeArea && sys.safeArea.bottom ? sys.windowHeight - sys.safeArea.bottom + 115 : 0 // #ifdef APP-PLUS appScreenshotListener = function() { if (groupGuard.screenshot && !canSendWhenMuted.value) { uni.showToast({ title: '本群已开启禁止截图', icon: 'none' }) } } try { plus.globalEvent.addEventListener('screenshot', appScreenshotListener) } catch (e) {} // #endif // #ifdef MP-WEIXIN if (uni.onUserCaptureScreen) { uni.onUserCaptureScreen(() => { if (groupGuard.screenshot && !canSendWhenMuted.value) { uni.showToast({ title: '本群已开启禁止截图', icon: 'none' }) } }) } // #endif }) onUnmounted(() => { socket.value?.close() socket.value = null // #ifdef APP-PLUS try { if (appScreenshotListener) { plus.globalEvent.removeEventListener('screenshot', appScreenshotListener) appScreenshotListener = null } } catch (e) {} // #endif // #ifdef MP-WEIXIN if (uni.offUserCaptureScreen) uni.offUserCaptureScreen() // #endif }) onShow(() => { const id = invite.value ? Creategroupid.value : groupId.value if (id) getGroupname(id) fetchHistory() }) // ✅ 新增:清空本地群聊记录(按 consumerId + groupId 精准) function clearCurrentGroupLocalHistory() { const gid = currentGroupId.value const cid = consumerId.value if (!cid || !gid) return uni.showModal({ title: '提示', content: '确定清空本地聊天记录吗?(不会删除服务器记录)', success: async (res) => { if (!res.confirm) return clearLocalGroupChatHistory(cid, gid) groupClearedAtISO.value = getGroupClearedAtISO(cid, gid) || '' messages.value = [] scrollToBottom() uni.showToast({ title: '已清空(本地)', icon: 'none' }) } }) } // ----------------------- // 群消息删除/撤回 // 19.castingChatGroupChatDeleteRecord // 入参:id(群消息id) // ----------------------- async function deleteGroupChatRecord(id) { const mid = String(id || '') if (!mid) throw new Error('消息ID缺失') const res = await ajax.post({ url: 'castingChatGroupChatDeleteRecord', method: 'post', data: { id: mid } }) if (res?.data?.result !== 'success') { throw new Error(res?.data?.remark || res?.data?.message || '删除失败') } return res?.data?.id || mid } function removeLocalMessageById(id) { const mid = String(id || '') if (!mid) return messages.value = messages.value.filter(m => String(m.castingChatId || '') !== mid) selectedIds.value = selectedIds.value.filter(x => x !== mid) } function findMessageById(id) { const mid = String(id || '') return messages.value.find(m => String(m.castingChatId || '') === mid) || null } function isSelfMessage(msg) { if (!msg) return false return String(msg.senderId || '') === String(consumerId.value || '') } function canRecallMessage(msg) { // groupchat=true => 不限时撤回 const flag = uni.getStorageSync('groupchat') const unlimited = flag === true || flag === 'true' || flag === 1 || flag === '1' if (unlimited) return true // 否则 3 分钟内 const t = Number(msg?.timestamp || 0) || (msg?.createTime ? new Date(msg.createTime).getTime() : 0) if (!t) return true return Date.now() - t <= 3 * 60 * 1000 } async function onBubbleDelete(payload) { const id = payload?.id if (!id) return uni.showToast({ title: '消息ID缺失', icon: 'none' }) const msg = findMessageById(id) if (!isSelfMessage(msg)) { return uni.showToast({ title: '只能删除自己发送的消息', icon: 'none' }) } uni.showLoading({ title: '处理中...' }) try { const deletedId = await deleteGroupChatRecord(id) removeLocalMessageById(deletedId) uni.showToast({ title: '已删除', icon: 'none' }) } catch (e) { uni.showToast({ title: e?.message || '删除失败', icon: 'none' }) } finally { uni.hideLoading() } } async function onBubbleRecall(payload) { const id = payload?.id if (!id) return uni.showToast({ title: '消息ID缺失', icon: 'none' }) const msg = findMessageById(id) if (!isSelfMessage(msg)) { return uni.showToast({ title: '只能撤回自己发送的消息', icon: 'none' }) } if (!canRecallMessage(msg)) { return uni.showToast({ title: '该消息已过三分钟无法撤回', icon: 'none' }) } uni.showLoading({ title: '处理中...' }) try { const deletedId = await deleteGroupChatRecord(id) removeLocalMessageById(deletedId) uni.showToast({ title: '已撤回', icon: 'none' }) } catch (e) { uni.showToast({ title: e?.message || '撤回失败', icon: 'none' }) } finally { uni.hideLoading() } } // 引用(引入) function safeParseDeep(input) { let data = input for (let i = 0; i < 3; i++) { if (typeof data === 'string') { const s = data.trim() try { if ((s.startsWith('{') && s.endsWith('}')) || (s.startsWith('[') && s.endsWith(']'))) { data = JSON.parse(s); continue } if (/^"(\\.|[^"])*"$/.test(s)) { data = JSON.parse(s); continue } } catch (e) {} break } if (typeof data === 'object' && data !== null) break } return data } function computePreviewText(raw) { if (!raw) return '' let s = raw if (typeof s !== 'string') { try { s = JSON.stringify(s) } catch { s = String(s) } } try { const obj = safeParseDeep(s) if (obj && typeof obj === 'object') { if (obj.msgType === 'text') return obj.text || '' if (obj.msgType === 'location') return `[位置] ${obj.name || obj.address || ''}` if (obj._kind === 'file') { const t = Number(obj.type || 0) const baseName = obj.showName || obj.fileName || '' if (t === 1) return baseName ? `[图片] ${baseName}` : '[图片]' if (t === 2) return baseName ? `[视频] ${baseName}` : '[视频]' if (t === 3) return baseName ? `[语音] ${baseName}` : '[语音]' if (t === 4) return baseName ? `[文件] ${baseName}` : '[文件]' } if (obj.msgType === 'forward_record' || obj.msgType === 'forward') return obj.title || '[聊天记录]' } } catch (e) {} return String(s).slice(0, 120) } function onBubbleIntroduce(payload) { const text = computePreviewText(payload?.content) if (!text) return inputText.value = `> ${text}\n` + (inputText.value || '') } // 多选入口(从 ChatBubble 点击“多选”触发) function onBubbleMultiSelect(payload) { const id = payload?.id if (!id) return if (!multiSelectMode.value) { multiSelectMode.value = true } const msg = messages.value.find(m => String(m.castingChatId || '') === String(id)) if (msg) toggleSelect(msg) } // ===== 群多选转发 / 收藏 ===== function openMultiForwardPicker() { if (!selectedIds.value.length) { return uni.showToast({ title: '请先选择消息', icon: 'none' }) } if (groupGuard.spread) { return uni.showToast({ title: '本群已禁止分享', icon: 'none' }) } multiForwardPickerShow.value = true } function openMultiCollectPicker() { if (!selectedIds.value.length) { return uni.showToast({ title: '请先选择消息', icon: 'none' }) } if (groupGuard.bookmark) { return uni.showToast({ title: '本群已禁止收藏', icon: 'none' }) } multiCollectPickerShow.value = true } // 构造群多选转发 payload(合并 / 逐条通用) function buildGroupForwardPayload(mode = 'merge') { const ids = [...selectedIds.value].map(String) const picked = messages.value .filter(m => ids.includes(String(m.castingChatId || ''))) .sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0)) .map(m => ({ castingChatGroupChatId: String(m.castingChatId || ''), senderId: String(m.senderId || ''), senderName: m.nickname || (String(m.senderId || '') === String(consumerId.value || '') ? (parame .value.nickname || '我') : '用户'), senderAvatar: String(m.senderId || '') === String(consumerId.value || '') ? (userAvatar.value || '') : (m.avatarUrl || ''), content: m.content, createTime: m.createTime || new Date(m.timestamp || Date.now()).toISOString() })) return { source: 'groupChat', mode, castingChatGroupId: currentGroupId.value, castingChatGroupChatIds: ids, items: picked, from: { groupId: currentGroupId.value, groupTitle: Groupname.value || '', selfId: String(consumerId.value || ''), selfName: parame.value.nickname || '我' }, maskMode: !!isMaskedGroup.value } } function gotoForwardfriend(mode = 'merge') { if (!selectedIds.value.length) { return uni.showToast({ title: '请先选择消息', icon: 'none' }) } if (groupGuard.spread) { return uni.showToast({ title: '本群已禁止分享', icon: 'none' }) } const payload = buildGroupForwardPayload(mode) multiForwardPickerShow.value = false exitMultiSelect() uni.navigateTo({ url: '/pages/sociAlize/forward/Forwardfriend?payload=' + encodeURIComponent(JSON.stringify(payload)) }) } // 多选批量删除(只允许删除自己的) async function batchDeleteSelected() { if (!selectedIds.value.length) return uni.showToast({ title: '请选择消息', icon: 'none' }) const ids = [...selectedIds.value].map(String) const selectedMsgs = messages.value.filter(m => ids.includes(String(m.castingChatId || ''))) const ownIds = selectedMsgs.filter(isSelfMessage).map(m => String(m.castingChatId || '')).filter(Boolean) const otherCount = ids.length - ownIds.length if (!ownIds.length) { return uni.showToast({ title: '只能删除自己发送的消息', icon: 'none' }) } uni.showLoading({ title: '删除中...' }) try { const tasks = ownIds.map(id => deleteGroupChatRecord(id)) const r = await Promise.allSettled(tasks) const okIds = r.filter(x => x.status === 'fulfilled').map(x => x.value) const ok = okIds.length const fail = r.length - ok okIds.forEach(id => removeLocalMessageById(id)) uni.showToast({ title: `已删除:${ok},失败:${fail}${otherCount ? `,他人消息不可删:${otherCount}` : ''}`, icon: 'none' }) if (!selectedIds.value.length) exitMultiSelect() } catch (e) { uni.showToast({ title: e?.message || '删除失败', icon: 'none' }) } finally { uni.hideLoading() } } // 多选批量撤回(只允许撤回自己的 + 时间规则) async function batchRecallSelected() { if (!selectedIds.value.length) return uni.showToast({ title: '请选择消息', icon: 'none' }) const ids = [...selectedIds.value].map(String) const selectedMsgs = messages.value.filter(m => ids.includes(String(m.castingChatId || ''))) const ownMsgs = selectedMsgs.filter(isSelfMessage) const otherCount = ids.length - ownMsgs.length const canRecallMsgs = ownMsgs.filter(canRecallMessage) const overtimeCount = ownMsgs.length - canRecallMsgs.length const recallIds = canRecallMsgs.map(m => String(m.castingChatId || '')).filter(Boolean) if (!recallIds.length) { return uni.showToast({ title: overtimeCount ? '该消息已过三分钟无法撤回' : '只能撤回自己发送的消息', icon: 'none' }) } uni.showLoading({ title: '撤回中...' }) try { const tasks = recallIds.map(id => deleteGroupChatRecord(id)) const r = await Promise.allSettled(tasks) const okIds = r.filter(x => x.status === 'fulfilled').map(x => x.value) const ok = okIds.length const fail = r.length - ok okIds.forEach(id => removeLocalMessageById(id)) uni.showToast({ title: `已撤回:${ok},失败:${fail}${overtimeCount ? `,超时不可撤回:${overtimeCount}` : ''}${otherCount ? `,他人消息不可撤回:${otherCount}` : ''}`, icon: 'none' }) if (!selectedIds.value.length) exitMultiSelect() } catch (e) { uni.showToast({ title: e?.message || '撤回失败', icon: 'none' }) } finally { uni.hideLoading() } } // 多选收藏(群) async function doMultiCollectGroup(isFileTransfer = false) { multiCollectPickerShow.value = false if (!selectedIds.value.length) { return uni.showToast({ title: '请选择消息', icon: 'none' }) } const cid = consumerId.value if (!cid) { return uni.showToast({ title: '请先登录', icon: 'none' }) } uni.showLoading({ title: '收藏中...' }) try { const ids = [...selectedIds.value].map(String) const tasks = ids.map(id => ajax.post({ url: 'castingChatCollect', method: 'post', data: { consumerId: cid, castingChatGroupChatId: id, fileTransfer: !!isFileTransfer } }) ) const results = await Promise.allSettled(tasks) const ok = results.filter(r => r.status === 'fulfilled').length const fail = results.length - ok uni.showToast({ title: `收藏成功:${ok},失败:${fail}`, icon: 'none' }) } catch (e) { uni.showToast({ title: '收藏失败', icon: 'none' }) } finally { uni.hideLoading() exitMultiSelect() } } const RemoveCoffee = ref(false) //移除咖密标识 onLoad(async (option) => { console.log(option, '群什么跳转也没有谁都能进的') // ✅ 兼容直接带 mask 参数 if (option?.mask !== undefined) { const v = option.mask isMaskedGroup.value = v === true || v === 'true' || v === 1 || v === '1' } if (option?.GroupID && option?.castingChatGroupId) { invite.value = false try { RemoveCoffee.value = true SelectgroupID.value = option.GroupID || option.castingChatGroupId await getGroupname(SelectgroupID.value) Groupname.value = decodeURIComponent(option.groupTitle) || '' groupId.value = SelectgroupID.value || '' if (groupId.value) { await fetchHistory() openSocket() } else { uni.showToast({ title: '缺少群ID', icon: 'none' }) } } catch (e) { uni.showToast({ title: '参数解析失败', icon: 'none' }) } return } console.log(option, '群来源的参数据') // 被邀请进群 if (option?.datalist && option?.Creategroupid) { invite.value = true try { const parsed = JSON.parse(decodeURIComponent(option.datalist)) Groupfriends.value.unshift(...parsed) Creategroupid.value = JSON.parse(decodeURIComponent(option.Creategroupid)) await getGroupname(Creategroupid.value) Groupname.value = Creategroupnickname.value SelectgroupID.value = Creategroupid.value if (Creategroupid.value) { await fetchHistory() openSocket() Getgroupchatmessages && Getgroupchatmessages() } else { uni.showToast({ title: '缺少群ID', icon: 'none' }) } } catch (e) {} return } // 从群列表进入(这里最重要:解析 groupparameters.mask) if (option?.groupparameters) { invite.value = false try { const p = JSON.parse(decodeURIComponent(option.groupparameters)) isMaskedGroup.value = p?.mask === true || p?.mask === 1 || p?.mask === '1' SelectgroupID.value = p.castingChatGroupId await getGroupname(SelectgroupID.value) Groupname.value = p.castingChatGroupTitle || '' groupId.value = SelectgroupID.value || '' if (groupId.value) { await fetchHistory() openSocket() } else { uni.showToast({ title: '缺少群ID', icon: 'none' }) } } catch (e) { uni.showToast({ title: '参数解析失败', icon: 'none' }) } return } // 群转发消息的入口 if (option?.Forwardmessage) { try { const p = JSON.parse(decodeURIComponent(option.Forwardmessage)) isMaskedGroup.value = p?.mask === true || p?.mask === 1 || p?.mask === '1' SelectgroupID.value = p.castingChatGroupId await getGroupname(SelectgroupID.value) Groupname.value = p.castingChatGroupTitle || '' groupId.value = SelectgroupID.value || '' if (groupId.value) { await fetchHistory() openSocket() } else { uni.showToast({ title: '缺少群ID', icon: 'none' }) } } catch (e) { uni.showToast({ title: '参数解析失败', icon: 'none' }) } return } // 从消息列表进入 if (option?.Messageid) { try { const MessageList = JSON.parse(decodeURIComponent(option.Messageid)) SelectgroupID.value = MessageList.id await getGroupname(SelectgroupID.value) Groupname.value = MessageList.name || '' groupId.value = SelectgroupID.value || '' if (groupId.value) { await fetchHistory() openSocket() } else { uni.showToast({ title: '缺少群ID', icon: 'none' }) } } catch (e) { uni.showToast({ title: '参数解析失败', icon: 'none' }) } return } }) </script> <style lang="scss" scoped> @import '@/assets/styles/global.scss'; .content { min-height: 100vh; position: relative; background-size: cover; } .overlay { position: absolute; inset: 0; background-size: cover; } .time-divider { width: 100%; text-align: center; color: #999; font-size: 24rpx; margin: 20rpx 0; } .header { width: 94%; margin: 0 auto; } .header-inner { position: relative; display: flex; align-items: center; justify-content: center; height: 100%; } .left-box, .right-box { position: absolute; top: 0; bottom: 0; width: 120rpx; display: flex; align-items: center; } .left-box { left: 0; justify-content: flex-start; } .right-box { right: 0; justify-content: flex-end; } .center-box { flex: none; } .title-text { font-size: 32rpx; color: #111; } /* 输入区 */ .input-area { width: 100%; height: 85rpx; display: flex; justify-content: center; align-items: center; position: absolute; bottom: 0; z-index: 99; } .input-left { width: 80%; height: 100%; display: flex; justify-content: space-between; box-sizing: border-box; } .input-box { height: 100%; width: 100%; background-color: #ffffff; } .input-box input { height: 100%; width: 100%; padding-left: 10rpx; } .input-right { width: 20%; display: flex; justify-content: space-between; box-sizing: border-box; height: 100%; } .btn-send { background-color: #d89833; width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; color: #fff; } /* 群管控提示 Banner */ .guard-banner { margin-top: 10rpx; margin-bottom: 10rpx; padding: 10rpx 16rpx; border-radius: 12rpx; background-color: rgba(0, 0, 0, 0.15); color: #fff; font-size: 22rpx; display: flex; flex-wrap: wrap; gap: 8rpx; } .guard-banner text { margin-right: 12rpx; } /* 消息行 + 多选 */ .msg-row { display: flex; align-items: flex-start; } .check-wrap { width: 64rpx; min-width: 64rpx; display: flex; justify-content: center; align-items: center; padding-top: 24rpx; } .bubble-wrap { flex: 1; } /* 多选操作条 */ .multi-action-bar { position: absolute; left: 0; right: 0; bottom: 0; z-index: 120; height: 120rpx; background-color: #ffffff; display: flex; align-items: center; justify-content: space-between; padding: 0 24rpx; box-sizing: border-box; border-top: 1rpx solid #eee; } .mab-left { font-size: 28rpx; color: #333; } .mab-right { display: flex; align-items: center; } .mode-popup { width: 686rpx; height: 240rpx; background-color: #ffffff; display: flex; justify-content: space-around; align-items: center; border-radius: 12rpx; } </style> 这个页面是主要的组聊天现在 这个组的websockets 后端还没写好 然后在这个页面点击上面的标题调到了这个页面 uni.navigateTo({text<!-- 聊天列表 --> <view style="width: 100%; display: flex; justify-content: center; align-items: center; height: 85%;"> <view style="width: 94%; height: 100%; position: relative;"> <!-- 群权限提示 Banner --> <view v-if="hasAnyGuard" class="guard-banner"> <text v-if="groupGuard.mute">已开启全员禁言,仅群主和管理员可发言</text> <text v-if="groupGuard.screenshot">已开启禁止截图</text> <text v-if="groupGuard.spread">已开启禁止分享</text> <text v-if="groupGuard.bookmark">已开启禁止收藏</text> <text v-if="groupGuard.copy">已开启禁止复制</text> </view> <!-- 邀请提示 --> <view v-if="invite"> <view v-if="Groupfriends" class="time-divider"> <text v-if="controltime">{{ formatChatTime(Date.now()) }}</text> </view> <view v-if="Groupfriends" style="width:auto;text-align:center;color:#999;font-size:24rpx;margin:20rpx 0;"> <text>您邀请了</text> <view style="display:inline-block;" v-for="(item, index) in Groupfriends" :key="index"> <text style="margin-left:10rpx;">{{ `${item.nickname},` }}</text> </view> </view> </view> <!-- 消息滚动区域 --> <view style="width: 100%; position: absolute; left: 0; right: 0; z-index: 9; height: 100%;" :style="{ top: containerTop + 'px', bottom: (bottomBarHeight) + 'px' }"> <scroll-view scroll-y :scroll-into-view="lastItemId" style="height: 100%;"> <view v-for="(msg, idx) in messages" :key="msg._localKey || idx" :id="'msg-' + idx"> <view v-if="shouldShowTimestamp(idx)" class="time-divider"> {{ formatChatTime(msg.timestamp) }} </view> <!-- 多选模式:每条消息左侧显示勾选框 --> <view class="msg-row"> <view v-if="multiSelectMode" class="check-wrap" @click.stop="toggleSelect(msg)"> <checkbox :checked="isSelected(msg.castingChatId)" /> </view> <view class="bubble-wrap" @click="multiSelectMode ? toggleSelect(msg) : null"> <ChatBubble :role="msg.role" :content="msg.content" :userAvatar="userAvatar" :assistantAvatar="msg.avatarUrl || assistantAvatar" :showCopyButton="false" :showBadge="false" :nickname="msg.nickname" :ownerNickname="parame.ownerNickname" :userid="msg.senderId" :consumerId="consumerId" :castingChatId="msg.castingChatId || ''" :castingChatGroupId="currentGroupId" :createTime="msg.createTime" :forbidSpread="groupGuard.spread" :forbidCopy="groupGuard.copy" :forbidBookmark="groupGuard.bookmark" :maskMode="isMaskedGroup" @delete="onBubbleDelete" @recall="onBubbleRecall" @introduce="onBubbleIntroduce" @multi-select="onBubbleMultiSelect" /> </view> </view> </view> </scroll-view> </view> </view> </view> </view> <!-- 多选操作条:转发 / 收藏 / 删除 / 撤回 / 取消 --> <view v-if="multiSelectMode" class="multi-action-bar"> <view class="mab-left">已选 {{ selectedCount }} 条</view> <view class="mab-right"> <up-button type="warning" style="height: 70rpx; width: 160rpx;" text="转发" @click="openMultiForwardPicker" /> <up-button type="warning" style="height: 70rpx; width: 160rpx; margin-left: 12rpx;" text="收藏" @click="openMultiCollectPicker" /> <up-button type="warning" style="height: 70rpx; width: 160rpx; margin-left: 12rpx;" text="删除" @click="batchDeleteSelected" /> <up-button type="warning" style="height: 70rpx; width: 160rpx; margin-left: 12rpx;" text="撤回" @click="batchRecallSelected" /> <up-button type="warning" style="height: 70rpx; width: 160rpx; margin-left: 12rpx;" text="取消" @click="exitMultiSelect" /> </view> </view> <!-- 输入框(多选模式隐藏) --> <view v-else class="input-area" style="display:flex;justify-content:center;align-items:center;"> <view style="width:100%;height:100%;display:flex;justify-content:center;align-items:center;"> <view class="input-left"> <view class="input-box"> <input v-model="inputText" :adjust-position="false" type="search" placeholder="请输入…" @confirm="onSend" /> </view> </view> <view class="input-right" v-if="inputText"> <view class="btn-send" @click="onSend">发送</view> </view> <view class="input-right" v-else> <view class="btn-send" @click="openAttachPanel"> <uni-icons type="plus-filled" size="26"></uni-icons> </view> </view> </view> </view> <!-- 附件弹层 --> <up-popup :show="attachShow" mode="bottom" @close="attachShow=false"> <view style="width:100%;padding:20rpx 30rpx;"> <view style="display:flex;align-items:center;"> <view style="text-align:center;" @click="pickAndSendImage"> <uni-icons type="image" size="30"></uni-icons> <view>图片</view> </view> <view style="text-align:center;margin-left:40rpx;" @click="pickAndSendFile"> <uni-icons type="wallet-filled" size="30"></uni-icons> <view>文件</view> </view> <view style="display: inline-block; margin-left: 40rpx; text-align: center" @click="OpencameraImage"> <uni-icons type="camera" size="30" /> <view>手机相机</view> </view> <view style="text-align:center;margin-left:40rpx;" @click="recordAndSendVoice"> <uni-icons type="mic-filled" size="30"></uni-icons> <view>语音</view> </view> <view style="display: inline-block;margin-left: 40rpx;text-align:center;" @click="Selectvideomedia"> <uni-icons type="camera" size="30"></uni-icons> <view>视频</view> </view> <!-- ✅ 新增:定位(对齐单聊 sharedspace + eventChannel) --> <view style="display: inline-block; margin-left: 40rpx; text-align: center" @click="openLocation"> <uni-icons type="location" size="30" /> <view>定位</view> </view> <!-- ✅ 新增:收藏分享(进入 collect 页,可选咖信/咖密) --> <view style="display: inline-block; margin-left: 40rpx; text-align: center" @click="openCollectShare(false)"> <uni-icons type="wallet-filled" size="30" /> <view>收藏分享</view> </view> <!-- ✅ 新增:文件分享(进入 collect 页,可选咖信/咖密文件分享) --> <view style="display: inline-block; margin-left: 40rpx; text-align: center" @click="openCollectShare(true)"> <uni-icons type="location" size="30" /> <view>文件分享</view> </view> </view> </view> </up-popup> </view> <!-- 群多选转发:合并 / 逐条 --> <up-popup :show="multiForwardPickerShow" mode="center" @close="multiForwardPickerShow=false" :closeable="true" closeIconPos="top-right" zIndex="99999" round="12"> <view class="mode-popup"> <up-button type="warning" style="height: 80rpx; width: 220rpx" text="合并转发" @click="gotoForwardfriend('merge')" /> <up-button style="height: 80rpx; width: 220rpx" text="逐条转发" @click="gotoForwardfriend('items')" /> </view> </up-popup> <!-- 群多选收藏:咖信 / 咖密 --> <up-popup :show="multiCollectPickerShow" mode="center" @close="multiCollectPickerShow=false" :closeable="true" closeIconPos="top-right" zIndex="99999" round="12"> <view class="mode-popup"> <up-button type="warning" style="height: 80rpx; width: 220rpx" text="咖信收藏" @click="doMultiCollectGroup(false)" /> <up-button type="warning" style="height: 80rpx; width: 220rpx" text="咖密收藏" @click="doMultiCollectGroup(true)" /> </view> </up-popup> </view>
</template> <script setup> import ajax from '@/api/ajax.js' import Selectfriends from '@/components/Selectfriends.vue' import { clearLocalGroupChatHistory, getGroupClearedAtISO } from '@/utils/system.js' import { getStatusBarHeight, getTitleBarHeight } from '@/utils/system.js' import { onShow, onLoad } from '@dcloudio/uni-app' import { ref, reactive, onMounted, watch, inject, nextTick, computed } from 'vue' import { useGroupsettings } from '@/store/useGroupsetup.js' import { storeToRefs } from 'pinia' import { useMainStore } from '@/store/index.js' import { usecontacts } from '@/store/usecontacts.js' const storects = usecontacts() const { Groupnickname, GroupID, SelectgroupID, keywords, PublicgroupID } = storeToRefs(storects) const { Personalsettings, switch1Change, Topup } = storects const useGroup = useGroupsettings() const { Groupfriends, isexpand, datasource, initialheight, manage, changestate, addFriend, Groupname } = storeToRefs(useGroup) const { sendStareChnage, Selectthree, selectitem, Acceptdata: AcceptFromStore } = useGroup function Acceptdata(list) { addFriend.value = list || [] const map = new Map() addFriend.value.forEach((n) => { const id = safeId(n) if (!map.has(id)) map.set(id, { ...n, id: n.id ?? n.consumerId }) }) addFriend.value = Array.from(map.values()) } // 全局主题 const store = useMainStore() const { theme } = storeToRefs(store) // 顶部安全区与标题栏高度 const statusBarHeight = ref(getStatusBarHeight()) const headerBarHeight = ref(getTitleBarHeight()) // 返回上一页 const navigateBack = () => { uni.navigateBack() } // 群公告 function Group() { uni.navigateTo({ url: '/pages/emitsgroupchat/GroupAnnouncement/GroupAnnouncement' }) } const parameterobject = ref({}) const show = ref(false) const popup = () => { show.value = true } function close() { show.value = false } function open() { show.value = true } // ✅ 新增:清空本地群聊记录(按 consumerId + groupId 精准) function clearCurrentGroupLocalHistory() { let consumerId = uni.getStorageSync('consumerId') const gid = SelectgroupID.value || TemporarygroupID.value const cid = consumerId if (!cid || !gid) return uni.showModal({ title: '提示', content: '确定清空本地聊天记录吗?(不会删除服务器记录)', success: async (res) => { if (!res.confirm) return clearLocalGroupChatHistory(cid, gid) // groupClearedAtISO.value = getGroupClearedAtISO(cid, gid) || '' uni.showToast({ title: '已清空(本地)', icon: 'none' }) setTimeout(() => { uni.navigateBack() }, 200) } }) } // 基础域名(与现网一致) const webProjectUrl = inject('webProjectUrl') // 页面内状态 const isinvite = ref(false) const inviteMode = ref('ordinary') // 'ordinary' | 'management' const nickname = ref('') // 群昵称 const Getgroupdetails = ref({}) // 群详细 const owner = ref('') const consumerId = uni.getStorageSync('consumerId') const avatar = ref('') // 群主头像 const TemporarygroupID = ref('') const userids = ref('') // 卡密这块 let hide = ref(false) //隐藏 const isAddlayers = ref(false) const andletters = ref(false) //数字字母 const ChangeHidden = (e) => { hide.value = e } /** * ✅ 群二次加密:跳 gesture 页面时只带 Groupsecondary * gesture 页面内部会识别为群模式:不拉取/不覆盖个人咖密,也不修改个人咖密 */ const Addlayerschange = (e) => { isAddlayers.value = !!e let Groupsecondary = { GroupID: SelectgroupID.value || TemporarygroupID.value, type: 1 } if (isAddlayers.value) { uni.navigateTo({ url: '/pages/sociAlize/Coffee/setpassword/gesture?Groupsecondary=' + encodeURIComponent(JSON .stringify(Groupsecondary)) + '&autoBack=1' }) } } /** * ✅ 群二次加密:跳 numbersletters 页面时只带 Grouptextencryption * numbersletters 页面内部会识别为群模式:只绑定 chatGroupSetting,不绑定个人 consumerSecretPassword */ const Makeletters = (e) => { let Grouptextencryption = { GroupID: SelectgroupID.value || TemporarygroupID.value, type: 2 } andletters.value = e if (andletters.value) { uni.navigateTo({ url: '/pages/sociAlize/Coffee/setpassword/numbersletters?Grouptextencryption=' + encodeURIComponent( JSON .stringify(Grouptextencryption)) + '&autoBack=1' }) } } // 群管控相关开关:全员禁言 / 禁止截图 / 分享 / 收藏 / 复制 const mute = ref(false) const forbidScreenshot = ref(false) const forbidSpread = ref(false) const forbidCopy = ref(false) const forbidBookmark = ref(false) /** 工具:统一 id(String) */ function safeId(obj) { return String(obj?.id ?? obj?.consumerId ?? '') } // 当前用户是否为管理员 const isManager = computed(() => Array.isArray(manage.value) ? manage.value.some((m) => safeId(m) === String(userids.value || '')) : false ) const canControlGuard = computed( () => String(userids.value || '') === String(owner.value || '') || isManager.value ) function ensureCanControlGuard() { if (!canControlGuard.value) { uni.showToast({ title: '只有群主或管理员可以修改该设置', icon: 'none' }) return false } return true } /** 拉群详情(作为“打开邀请面板”和“管控开关”前置步骤) */ function Getgroupchatmessages() { const parameter = { castingChatGroupId: SelectgroupID.value || TemporarygroupID.value, consumerId: uni.getStorageSync('consumerId') } PublicgroupID.value = parameter.castingChatGroupId return ajax .post({ url: 'castingChatGroupShow', data: parameter, method: 'post' }) .then((res) => { if (res.data.result === 'success') { const data = res.data Getgroupdetails.value = data owner.value = data.ownerId Groupfriends.value = Array.isArray(data.consumerArray) ? data.consumerArray : [] nickname.value = data.nickname || '' manage.value = Array.isArray(data.managerArray) ? data.managerArray : [] avatar.value = data.ownerAvatarUrl || '' // 同步群管控状态 mute.value = !!data.mute forbidScreenshot.value = !!data.screenshot forbidSpread.value = !!data.spread forbidCopy.value = !!data.copy forbidBookmark.value = !!data.bookmark } }) .catch(() => {}) } /** —— 打开邀请面板:成员 —— */ async function openInviteMembers() { await Getgroupchatmessages() // 刷新一次 inviteMode.value = 'ordinary' await nextTick() changestate.value = true selectRef.value?.resetSelection?.() } /** —— 打开邀请面板:管理员 —— */ async function openInviteManagers() { if (userids.value !== owner.value) { uni.showToast({ title: '只有群主可以添加管理员', icon: 'none' }) return } await Getgroupchatmessages() inviteMode.value = 'management' await nextTick() changestate.value = true selectRef.value?.resetSelection?.() } /** 取消邀请面板 */ function cancelInvite() { changestate.value = false addFriend.value = [] } /** 群管控开关变更 */ async function onToggleMute(val) { if (!ensureCanControlGuard()) { mute.value = !val return } mute.value = !!val await Modifygroupchat({}) } const Removecoffeegroup = () => { let consumerId = uni.getStorageSync('consumerId') let option = { castingChatGroupId: SelectgroupID.value || TemporarygroupID.value, consumerId, castingSecret: false } ajax.post({ url: 'chatGroupSetting', method: 'post', data: option }).then(res => { if (res.data.result === 'success') { uni.showToast({ title: '移除成功', icon: 'none' }) } }) } async function onToggleScreenshot(val) { if (!ensureCanControlGuard()) { forbidScreenshot.value = !val return } forbidScreenshot.value = !!val await Modifygroupchat({}) } async function onToggleSpread(val) { if (!ensureCanControlGuard()) { forbidSpread.value = !val return } forbidSpread.value = !!val await Modifygroupchat({}) } async function onToggleBookmark(val) { if (!ensureCanControlGuard()) { forbidBookmark.value = !val return } forbidBookmark.value = !!val await Modifygroupchat({}) } async function onToggleCopyLimit(val) { if (!ensureCanControlGuard()) { forbidCopy.value = !val return } forbidCopy.value = !!val await Modifygroupchat({}) } async function invite() { const raw = addFriend.value || [] const selectedIds = raw .map((it) => it?.id ?? it?.consumerId) .filter(Boolean) .map((v) => String(v)) if (!selectedIds.length) { uni.showToast({ title: '请先选择联系人', icon: 'none' }) return } const memberIdSet = new Set((Groupfriends.value || []).map((x) => safeId(x))) const managerIdSet = new Set((manage.value || []).map((x) => safeId(x))) let memberIdsToAdd = [] let managerIdsToAdd = [] if (inviteMode.value === 'ordinary') { memberIdsToAdd = selectedIds.filter((id) => !memberIdSet.has(id)) if (!memberIdsToAdd.length) { uni.showToast({ title: '没有可邀请的成员', icon: 'none' }) return } } else { managerIdsToAdd = selectedIds.filter((id) => !managerIdSet.has(id)) if (!managerIdsToAdd.length) { uni.showToast({ title: '选择的都已是管理员', icon: 'none' }) return } memberIdsToAdd = selectedIds.filter((id) => !memberIdSet.has(id)) } try { const res = await Modifygroupchat({ memberIds: memberIdsToAdd, managerIds: managerIdsToAdd, mode: inviteMode.value }) if (res?.data?.result === 'success') { const pickById = (arr, id) => arr.find((n) => safeId(n) === String(id)) if (memberIdsToAdd.length) { const toAddMembers = memberIdsToAdd .map((id) => pickById(raw, id)) .filter(Boolean) .map((n) => ({ ...n, id: n.id ?? n.consumerId })) const map = new Map(Groupfriends.value.map((x) => [safeId(x), x])) toAddMembers.forEach((n) => { if (!map.has(safeId(n))) map.set(safeId(n), n) }) Groupfriends.value = Array.from(map.values()) } if (managerIdsToAdd.length) { const toAddManagers = managerIdsToAdd .map((id) => pickById(raw, id)) .filter(Boolean) .map((n) => ({ ...n, id: n.id ?? n.consumerId })) const mapM = new Map(manage.value.map((x) => [safeId(x), x])) toAddManagers.forEach((n) => { if (!mapM.has(safeId(n))) mapM.set(safeId(n), n) }) manage.value = Array.from(mapM.values()) } changestate.value = false addFriend.value = [] uni.showToast({ title: '已提交', icon: 'success' }) await Getgroupchatmessages() } else { uni.showToast({ title: '操作失败', icon: 'none' }) } } catch (e) { uni.showToast({ title: '网络错误', icon: 'none' }) } } /** 修改群聊(统一入口:成员、管理员、群管控 5 开关) */ function Modifygroupchat({ memberIds = [], managerIds = [], mode = 'ordinary' }) { const option = { id: SelectgroupID.value || TemporarygroupID.value, title: keywords.value, // 按原有逻辑透传 notice: '', consumerIds: memberIds, // 新增成员 ownerId: consumerId, managerIds: managerIds, // 新增管理员 cancelManagerIds: '', // 新增:群管控 mute: mute.value, screenshot: forbidScreenshot.value, spread: forbidSpread.value, copy: forbidCopy.value, bookmark: forbidBookmark.value } return ajax .post({ url: 'castingChatGroup', data: option, method: 'post' }) .then((res) => res) .catch(() => { uni.showToast({ title: '修改失败', icon: 'none' }) }) } // 群相关跳转 function findrecord() { uni.navigateTo({ url: '/pages/emitsgroupchat/GroupAnnouncement/chathistory' }) } function Changegroupname() { if (userids.value === owner.value) { uni.navigateTo({ url: '/pages/emitsgroupchat/Groupsettings/Modifygroup?GroupID=' + decodeURIComponent(JSON.stringify(parameterobject.value)) }) } else { uni.showToast({ title: '只有群主可以修改群名', icon: 'none' }) } } function Groupcode() { uni.navigateTo({ url: '/pages/emitsgroupchat/Groupsettings/GroupQRcode' }) } function chnageName() { uni.navigateTo({ url: '/pages/emitsgroupchat/Groupsettings/Nicknamegroup?GroupID=' + decodeURIComponent(JSON.stringify(parameterobject.value)) }) } // 解散群聊 function disband() { uni.showModal({ title: '解散群聊', content: '您确定要解散群聊吗', confirmText: '确定', success: (res) => { if (res.confirm) Disbandgroupchat() } }) } function Disbandgroupchat() { if (userids.value === owner.value) { const option = { castingChatGroupId: SelectgroupID.value || TemporarygroupID.value, consumerId: userids.value } ajax .post({ url: 'deleteCastingChatGroup', data: option, method: 'post' }) .then((res) => { if (res.data.result === 'success') { uni.showToast({ title: '解散成功' }) Returnpage() } }) } else { uni.showToast({ title: '抱歉,您当前无权限', icon: 'none' }) } } const Returnpage = () => { return uni.navigateTo({ url: '/pages/sociAlize/sociAlize' }) } const selectRef = ref(null) onMounted(() => { parameterobject.value = { PublicgroupID: PublicgroupID.value, avatar: avatar.value } }) onShow(async () => { await Getgroupchatmessages() }) const RemoveCoffee = ref(false) //按钮的存在 onLoad((option) => { if (option?.GroupID) { let obj = JSON.parse(decodeURIComponent(option.GroupID)) TemporarygroupID.value = obj.SelectgroupID RemoveCoffee.value = obj.RemoveCoffee } userids.value = uni.getStorageSync('consumerId') Getgroupchatmessages() }) function Showcontacts() { uni.showModal({ title: '清空聊天记录', content: '确认清空本群聊天记录?', success: (res) => { if (res.confirm) { } } }) } // 占位:隐藏区保存表单 function openAddForm() { uni.showToast({ title: '开发中', icon: 'none' }) } </script> <style lang="scss" scoped> @import '@/assets/styles/global.scss'; .app-container {} .content { min-height: 100vh; position: relative; background-size: cover; } .overlay { position: absolute; inset: 0; background-size: cover; } .header { width: 94%; margin: 0 auto; } .header-inner { position: relative; display: flex; align-items: center; justify-content: center; height: 100%; } .left-box, .right-box { position: absolute; top: 0; bottom: 0; width: 120rpx; display: flex; align-items: center; } .left-box { left: 0; justify-content: flex-start; } .right-box { right: 0; justify-content: flex-end; } .center-box { flex: none; } .center { width: 100%; display: flex; justify-content: center; align-items: center; } .inner { width: 94%; height: 100%; } .card { width: 100%; background-color: #ffffff; border-radius: 12rpx; margin-top: 0rpx; } .content-box { overflow: hidden; transition: max-height 0.3s ease; box-sizing: border-box; } .row { border-radius: 12rpx; background-color: #ffffff; height: 78rpx; display: flex; justify-content: center; align-items: center; margin-top: 32rpx; } .row.clickable { cursor: pointer; } .row-inner { width: 93%; height: 40rpx; display: flex; justify-content: space-between; box-sizing: border-box; align-items: center; } .row-left-text { width: 30%; } .row-left-text.shrink { width: 25%; } .row-right-slot { width: 70%; height: 100%; display: flex; align-items: center; justify-content: flex-end; gap: 12rpx; } .row-right-icon { width: 50%; display: flex; justify-content: flex-end; align-items: center; } .qrcode { height: 48rpx; width: 48rpx; border-radius: 12rpx; } .ellipsis { max-width: 520rpx; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .btn-danger { border-radius: 12rpx; background-color: #ffffff; width: 100%; height: 78rpx; display: flex; justify-content: center; align-items: center; margin-top: 32rpx; color: #da2822; } .btn-cancel { height: 54rpx; border-radius: 20rpx; width: 200rpx; background-color: #eee; display: flex; justify-content: center; align-items: center; color: #333; } .btn-primary { height: 54rpx; border-radius: 20rpx; width: 200rpx; background-color: #add8e6; display: flex; justify-content: center; align-items: center; color: #ffffff; } </style>这个页面目前还是群的详情你要根据组的内容来对这个页面修改修改根据图片展示到时候我会你给图片的 群和管理看的内容 是一样的然后 组员看到的少一点texturl:'pages/sociAlize/Coffeegroup/Groupchat/GroupDetails' }) GroupDetails <template> <view :class="['app-container', theme]"> <view class="content"> <view class="overlay"> <!-- 顶部 Header --> <view class="header" :style="{ paddingTop: statusBarHeight + 'px', height: headerBarHeight + 'px' }"> <view class="header-inner"> <!-- 左:返回按钮 --> <view v-if="!changestate" class="left-box" @click="navigateBack"> <uni-icons type="left" color="#000000" size="26"></uni-icons> </view> <!-- 中:标题 --> <view class="center-box"> <text class="title-text">{{ Getgroupdetails.title }}</text> </view> <!-- 右:说明 --> <view class="right-box"> <text class="desc-text">说明</text> </view> </view> </view> <!-- 页面主体 --> <view class="center"> <view class="inner"> <scroll-view style="height: 86vh;" scroll-y="true"> <template v-if="!changestate"> <!-- 成员九宫格 + 添加 --> <view class="card"> <view style="padding: 20rpx;"> <view class="content-box" :style="{height: initialheight + 'rpx'}" style="display: grid;grid-template-columns: repeat(5, 110rpx);gap: 32rpx;"> <!-- 好友头像 --> <view v-for="(items, indexs) in Groupfriends" :key="safeId(items)" @click="selectitem(items, indexs)" style="width: 100rpx; height: 100rpx;border-radius: 12rpx;"> <view style="width: 100rpx;height: 100rpx;background-color: #cccccc;border-radius: 12rpx;"> <image :src="webProjectUrl + items.avatarUrl" mode="aspectFill" style="height: 100%; width: 100%; border-radius: 12rpx;" /> </view> </view> <!-- 添加成员 --> <view @click.stop="openInviteMembers" style="width: 100rpx;height: 100rpx;border-radius: 12rpx;background-color: #cccccc;display: flex;justify-content: center;align-items: center;"> <view style="font-size: 60rpx; font-weight: bold;">+</view> </view> </view> <view style="width: 100%;display: flex;justify-content: center;align-items: center;"> <view>{{ !isexpand ? '收起' : '查看更多' }}</view> <view @click="sendStareChnage()"> <uni-icons :type="isexpand?'up':'down'" color="#cccccc" size="30"></uni-icons> </view> </view> </view> </view> <!-- 群管理(横向滚动) --> <view style="margin-top: 16rpx;" class="card"> <view style="padding: 20rpx;"> <scroll-view scroll-x="true" style="width: 100%;white-space: nowrap;overflow: scroll;margin-top: 16rpx;"> <view style="width: 100%;display: flex;flex-direction: row;align-items: center;box-sizing: border-box;"> <!-- 已有管理员头像 --> <view v-for="(items, indexs) in manage" :key="safeId(items)" @click="Selectthree(items, indexs)" :style="{display:'flex',alignItems:'center', marginLeft: indexs === 0 ? '0' : '32rpx'}"> <view style="border-radius: 12rpx; height: 100rpx;width: 100rpx;background-color: #cccccc;"> <image :src="webProjectUrl + items.avatarUrl" mode="aspectFill" style="height: 100%;width: 100%;border-radius: 12rpx;" /> </view> </view> <!-- 添加管理员(打开邀请面板) --> <view @click.stop="openInviteManagers" style="display: flex;align-items: center;" :style="{marginLeft:manage && manage.length ? '32rpx':'0rpx'}"> <view style="width: 100rpx;height: 100rpx; border-radius: 12rpx;background-color: #cccccc;display: flex;justify-content: center;align-items: center;"> <view style="font-size: 60rpx; font-weight: bold;">+</view> </view> </view> </view> </scroll-view> </view> </view> <!-- 群名称 --> <view class="row clickable" @click="Changegroupname()"> <view class="row-inner"> <view class="row-left-text">群名称</view> <view class="row-right-slot"> <view class="ellipsis">{{ Getgroupdetails.title }}</view> <uni-icons type="right" size="25"></uni-icons> </view> </view> </view> <!-- 群二维码 --> <view class="row clickable" @click="Groupcode()"> <view class="row-inner"> <view class="row-left-text">群二维码</view> <view class="row-right-slot"> <view class="qrcode"> <image style="height: 100%;width: 100%;" src="https://img0.baidu.com/it/u=582752653,3386538207&fm=253&fmt=auto&app=138&f=GIF?w=500&h=500" mode="aspectFill" /> </view> </view> </view> </view> <!-- 群公告 --> <view class="row clickable" @click="Group()"> <view class="row-inner"> <view class="row-left-text">群公告</view> <view class="row-right-icon"> <uni-icons type="right" size="25"></uni-icons> </view> </view> </view> <!-- 在本群名称 --> <view class="row clickable" @click="chnageName()"> <view class="row-inner"> <view class="row-left-text shrink">在本群名称</view> <view class="row-right-slot"> <view>{{ nickname }}</view> <uni-icons type="right" size="25"></uni-icons> </view> </view> </view> <!-- 查找聊天记录 --> <view class="row clickable" @click="findrecord()"> <view class="row-inner"> <view class="row-left-text">查找聊天记录</view> <view class="row-right-slot"> <view>空</view> <uni-icons type="right" size="25"></uni-icons> </view> </view> </view> <!-- 消息免打扰 --> <view class="row"> <view class="row-inner"> <view class="row-left-text">消息免打扰</view> <view class="row-right-icon"> <switch @change="switch1Change" checked color="#FFCC33" style="transform:scale(0.7)" /> </view> </view> </view> <!-- 置顶聊天 --> <view class="row"> <view class="row-inner"> <view class="row-left-text">置顶聊天</view> <view class="row-right-icon"> <switch @change="Topup" checked color="#FFCC33" style="transform:scale(0.7)" /> </view> </view> </view> <view v-if="RemoveCoffee" style="height:76rpx;width:100%;margin-top: 32rpx;" class=""> <up-button style="height: 100%;width: 100%;" type="warning" @click="Removecoffeegroup" text="移除咖密群"></up-button> </view> <!-- 清空聊天记录 --> <view style="width: 100%;height: 76rpx;margin-top: 32rpx;"> <up-button @click="clearCurrentGroupLocalHistory()" type="warning" style="height:100%; width:100%" text="清空聊天记录" /> </view> <!-- 解散群聊 --> <view class="btn-danger" @click="disband()">解散群聊</view> <!-- 添加到隐藏区 --> <view style="width: 100%;height: 72rpx;display: flex;justify-content: center;align-items: center;border-radius: 12rpx;margin-top: 16rpx;background-color: rgba(255,255, 255, 0.3);"> <view style="height: 60%;width: 90%;"> <view style="width: 100%;display: flex;justify-content: space-between;box-sizing: border-box;"> <view style="width: 50%;display: flex;box-sizing: border-box;"> <view style="width: 100%;height: 100%;position: relative;"> <view style="position: absolute;left: 0%;"> 添加到隐藏区 </view> </view> </view> <view style="width: 50%;display: flex;box-sizing: border-box;"> <view style="width: 100%;height: 100%;position: relative;"> <view style="position: absolute;right: 0%;"> <up-switch v-model="hide" @change="ChangeHidden"></up-switch> </view> </view> </view> </view> </view> </view> <!-- 隐藏区密码设置 --> <view v-if="hide" style="width: 100%;"> <view style="margin-top: 16rpx;">设置密码</view> <!-- 手势密码解锁 --> <view style="width: 100%;height: 72rpx;display: flex;justify-content: center;align-items: center;border-radius: 12rpx;background-color: white;margin-top: 32rpx;"> <view style="height: 60%;width: 90%;"> <view style="width: 100%;display: flex;justify-content: space-between;box-sizing: border-box;"> <view style="width: 50%;display: flex;box-sizing: border-box;"> <view style="width: 100%;height: 100%;position: relative;"> <view style="position: absolute;left: 0%;"> 手势密码解锁 </view> </view> </view> <view style="width: 50%;display: flex;box-sizing: border-box;"> <view style="width: 100%;height: 100%;position: relative;"> <view style="position: absolute;right: 0%;"> <up-switch v-model="isAddlayers" @change="Addlayerschange"></up-switch> </view> </view> </view> </view> </view> </view> <!-- 数字+字母密码解锁 --> <view style="width: 100%;height: 72rpx;display: flex;justify-content: center;align-items: center;border-radius: 12rpx;background-color: white;margin-top: 16rpx;"> <view style="height: 60%;width: 90%;"> <view style="width: 100%;display: flex;justify-content: space-between;box-sizing: border-box;"> <view style="width: 50%;display: flex;box-sizing: border-box;"> <view style="width: 100%;height: 100%;position: relative;"> <view style="position: absolute;left: 0%;"> 数字+字母密码解锁 </view> </view> </view> <view style="width: 50%;display: flex;box-sizing: border-box;"> <view style="width: 100%;height: 100%;position: relative;"> <view style="position: absolute;right: 0%;"> <up-switch v-model="andletters" @change="Makeletters"></up-switch> </view> </view> </view> </view> </view> </view> <!-- 确认/取消加密 --> <view style="width: 100%;display: flex;justify-content: space-between;box-sizing: border-box;height: 72rpx;margin-top: 40rpx;"> <view style="width: 50%;display: flex;box-sizing: border-box;"> <view style="width: 100%;height: 100%;position: relative;"> <view style="position: absolute;left: 0%;"> <up-button style="height: 72rpx;width: 162rpx;color: black;" @click="openAddForm" text="取消加密"></up-button> </view> </view> </view> <view style="width: 50%;display: flex;box-sizing: border-box;"> <view style="width: 100%;height: 100%;position: relative;"> <view style="position: absolute;right: 0%;"> <up-button type="warning" style="height: 72rpx;width: 162rpx;" @click="openAddForm" text="确定加密"></up-button> </view> </view> </view> </view> </view> <!-- 管理禁言群区域 --> <view style="width: 100%;margin-top: 32rpx;"> <!-- 标题 + 说明 ? --> <view style="display:flex;align-items:center;"> (管理)禁言群 <view @click="popup()" style="height:35rpx;width:35rpx;border-radius:50%;margin-left:10rpx;background-color:white;display:flex;justify-content:center;align-items:center;"> ? </view> </view> <!-- 全员禁言 --> <view style="width: 100%;height: 72rpx;display: flex;justify-content: center;align-items: center;border-radius: 12rpx;background-color: white;margin-top: 16rpx;"> <view style="height: 60%;width: 90%;"> <view style="width: 100%;display: flex;justify-content: space-between;box-sizing: border-box;"> <view style="width: 50%;display: flex;box-sizing: border-box;"> <view style="width: 100%;height: 100%;position: relative;"> <view style="position: absolute;left: 0%;"> 全员禁言 </view> </view> </view> <view style="width: 50%;display: flex;box-sizing: border-box;"> <view style="width: 100%;height: 100%;position: relative;"> <view style="position: absolute;right: 0%;"> <up-switch v-model="mute" @change="onToggleMute"></up-switch> </view> </view> </view> </view> </view> </view> <!-- 禁止截图 --> <view style="width: 100%;height: 72rpx;display: flex;justify-content: center;align-items: center;border-radius: 12rpx;background-color: white;margin-top: 16rpx;"> <view style="height: 60%;width: 90%;"> <view style="width: 100%;display: flex;justify-content: space-between;box-sizing: border-box;"> <view style="width: 50%;display: flex;box-sizing: border-box;"> <view style="width: 100%;height: 100%;position: relative;"> <view style="position: absolute;left: 0%;"> 禁止截图 </view> </view> </view> <view style="width: 50%;display: flex;box-sizing: border-box;"> <view style="width: 100%;height: 100%;position: relative;"> <view style="position: absolute;right: 0%;"> <up-switch v-model="forbidScreenshot" @change="onToggleScreenshot"></up-switch> </view> </view> </view> </view> </view> </view> <!-- 禁止分享 --> <view style="width: 100%;height: 72rpx;display: flex;justify-content: center;align-items: center;border-radius: 12rpx;background-color: white;margin-top: 16rpx;"> <view style="height: 60%;width: 90%;"> <view style="width: 100%;display: flex;justify-content: space-between;box-sizing: border-box;"> <view style="width: 50%;display: flex;box-sizing: border-box;"> <view style="width: 100%;height: 100%;position: relative;"> <view style="position: absolute;left: 0%;"> 禁止分享(转发) </view> </view> </view> <view style="width: 50%;display: flex;box-sizing: border-box;"> <view style="width: 100%;height: 100%;position: relative;"> <view style="position: absolute;right: 0%;"> <up-switch v-model="forbidSpread" @change="onToggleSpread"></up-switch> </view> </view> </view> </view> </view> </view> <!-- 禁止收藏 --> <view style="width: 100%;height: 72rpx;display: flex;justify-content: center;align-items: center;border-radius: 12rpx;background-color: white;margin-top: 16rpx;"> <view style="height: 60%;width: 90%;"> <view style="width: 100%;display: flex;justify-content: space-between;box-sizing: border-box;"> <view style="width: 50%;display: flex;box-sizing: border-box;"> <view style="width: 100%;height: 100%;position: relative;"> <view style="position: absolute;left: 0%;"> 禁止收藏 </view> </view> </view> <view style="width: 50%;display: flex;box-sizing: border-box;"> <view style="width: 100%;height: 100%;position: relative;"> <view style="position: absolute;right: 0%;"> <up-switch v-model="forbidBookmark" @change="onToggleBookmark"></up-switch> </view> </view> </view> </view> </view> </view> <!-- 禁止复制 --> <view style="width: 100%;height: 72rpx;display: flex;justify-content: center;align-items: center;border-radius: 12rpx;background-color: white;margin-top: 16rpx;margin-bottom:32rpx;"> <view style="height: 60%;width: 90%;"> <view style="width: 100%;display: flex;justify-content: space-between;box-sizing: border-box;"> <view style="width: 50%;display: flex;box-sizing: border-box;"> <view style="width: 100%;height: 100%;position: relative;"> <view style="position: absolute;left: 0%;"> 禁止复制 </view> </view> </view> <view style="width: 50%;display: flex;box-sizing: border-box;"> <view style="width: 100%;height: 100%;position: relative;"> <view style="position: absolute;right: 0%;"> <up-switch v-model="forbidCopy" @change="onToggleCopyLimit"></up-switch> </view> </view> </view> </view> </view> </view> </view> </template> <!-- 邀请面板 --> <template v-else> <view style="height:82vh;"> <scroll-view scroll-y style="height:82vh;"> <Selectfriends ref="selectRef" :isinvite="isinvite" :mode="inviteMode" :existingMemberIds="Groupfriends.map(i => safeId(i))" :existingManagerIds="manage.map(i => safeId(i))" @seddata="Acceptdata" /> </scroll-view> </view> </template> </scroll-view> </view> </view> <!-- 底部操作条:取消 + 立即邀请 --> <view v-if="changestate" style="position:absolute;bottom:0;height:112rpx;width:100%;background-color:#ffffff;display:flex;justify-content:center;align-items:center;"> <view style="height:72rpx;width:90%;display:flex;justify-content:space-between;align-items:center;"> <!-- 取消 --> <view @click="cancelInvite" class="btn-cancel">取消</view> <!-- 立即邀请 --> <view @click.stop="invite" class="btn-primary">立即邀请{{ addFriend.length }}</view> </view> </view> </view> </view> <!-- 阅后即焚说明书弹窗 --> <up-popup :show="show" @close="close" mode="center" :closeable="true" :safeAreaInsetTop="true" closeIconPos="top-right" zIndex="99999" round="12" @open="open"> <view style="width: 686rpx;height: 752rpx;background-color: white;display: flex;justify-content: center;align-items: center;border-radius: 12rpx;"> <!-- 滑动区域 --> <scroll-view scroll-y style="width: 100%;height: 100%;box-sizing: border-box;padding: 24rpx;"> <view>阅后即焚说明书</view> <view style="min-height: 100%;"> 内容上传用于构建知识库的非结构化数据时: • 该字段代表上传文档的名称,注意后缀需要带上文档格式类型。支持格式:pdf、docx、doc、txt、md、pptx、ppt、xlsx、xls、html、png、jpg、jpeg、bmp、gif。 文档名称长度限制 4 - 128 个字符。 说明 如需创建结构化数据表并上传数据,请使用阿里云百炼控制台,API 不支持。 上传用于智能体应用会话交互的文件时: • 该字段代表上传文件的名称,注意后缀需要带上文件格式类型。支持格式: ○ 文档:doc、docx、wps、ppt、pptx、xls、xlsx、md、txt、pdf。 ○ 图片:png、jpg、jpeg、bmp、gif。 ○ 音频:aac、amr、flac、flv、m4a、mp3、mpeg、ogg、opus、wav、webm、wma。 ○ 视频:mp4、mkv、avi、mov、wmv。 ○ 文件名称长度限制 4 - 128 个字符。 </view> </scroll-view> </view> </up-popup> </view>
到时候 我给两张图片给你的你对他们的功能区别和限制 这个是组聊天的气泡组件groupchatbubble<!-- components/ChatBubble.vue -->
<template>
<view>
<view :class="['chat-message', roleClass]">
<!-- 头像 -->
<image class="avatar" :src="avatarSrc" mode="aspectFill" @click="onAvatarClick" />
</template> <script setup> import MyVideo from '@/components/HomepageComponents/Recommend/MyVideo.vue' import { ref, computed } from 'vue' import ajax from '@/api/ajax.js' const MASK_AVATAR_URL = 'https://img1.baidu.com/it/u=4080682647,2563443672&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500' const props = defineProps({ role: { type: String, required: true, validator: (v) => ['user', 'assistant', 'typing', 'system'].includes(v) }, content: { type: [String, Object], default: '' }, userAvatar: { type: String, default: '' }, assistantAvatar: { type: String, default: '' }, showCopyButton: { type: Boolean, default: false }, showBadge: { type: Boolean, default: false }, nickname: { type: String, default: '' }, ownerNickname: { type: String, default: '' }, userid: { type: String, default: '' }, // senderId consumerId: { type: String, default: '' }, // 当前用户 castingChatId: { type: String, default: '' }, castingChatGroupId: { type: String, default: '' }, createTime: { type: String, default: '' }, forbidSpread: { type: Boolean, default: false }, forbidCopy: { type: Boolean, default: false }, forbidBookmark: { type: Boolean, default: false }, // ✅ 新增:是否打码群 maskMode: { type: Boolean, default: false } }) const emit = defineEmits(['delete', 'recall', 'introduce', 'multi-select']) // 弹窗 & 长按控制 const timer = ref(null) const show = ref(false) const Iscollecting = ref(false) const preventImageClick = ref(false) const Theabovefunction = ref([{ title: '转发', id: '1' }, { title: '收藏', id: '2' }, { title: '删除', id: '3' } ]) const Thefollowingfunctions = ref([{ title: '引入', id: '1' }, { title: '多选', id: '2' }, { title: '撤回', id: '3' } ]) function close() { show.value = false Iscollecting.value = false } function open() { show.value = true } // 长按 2 秒弹出 function handleTouchStart() { if (timer.value) clearTimeout(timer.value) preventImageClick.value = false timer.value = setTimeout(() => { preventImageClick.value = true longPressAction() }, 2000) } function handleTouchEnd() { if (timer.value) { clearTimeout(timer.value) timer.value = null } } function longPressAction() { show.value = true } function safeParseDeep(input) { let data = input for (let i = 0; i < 4; i++) { if (typeof data === 'string') { const s = data.trim() try { if ((s.startsWith('{') && s.endsWith('}')) || (s.startsWith('[') && s.endsWith(']'))) { data = JSON.parse(s) continue } if (/^"(\\.|[^"])*"$/.test(s)) { data = JSON.parse(s) continue } } catch (e) {} break } if (typeof data === 'object' && data !== null) break } return data } function normalizeParsed(input) { let o = safeParseDeep(input) for (let i = 0; i < 4; i++) { if (o && typeof o === 'object') { if (o.msgType || o._kind || o.type) return o if ('content' in o && !('msgType' in o) && !('_kind' in o)) { o = safeParseDeep(o.content) continue } } break } return o } function asString(x) { try { return typeof x === 'string' ? x : JSON.stringify(x) } catch { return String(x || '') } } function normalizeContentForForward() { if (typeof props.content === 'string') return props.content return asString(props.content) } function maskText(str = '') { const s = String(str || '') const n = Array.from(s).length return n ? '*'.repeat(n) : '*' } // 全局前缀 const urls = ref(getApp().globalData.webProjectUrl || '') const isSelf = computed(() => !!props.userid && !!props.consumerId && props.userid === props.consumerId) const isGroupMode = computed(() => !!props.castingChatGroupId) const isGroupMate = computed(() => !isSelf.value && !!props.userid) const isAssistantLike = computed(() => props.role === 'assistant' || props.role === 'system') const roleClass = computed(() => (props.role === 'user' ? 'user' : 'assistant')) // ✅ 群聊:强制显示昵称;非群聊:有 nickname 才显示 const showNicknameBar = computed(() => { if (isGroupMode.value) return true return !!props.nickname }) const displayNickname = computed(() => { const raw = props.nickname || (isSelf.value ? '我' : '用户') return props.maskMode ? maskText(raw) : raw }) function fullUrl(u = '') { if (!u) return '' if (/^https?:\/\//i.test(u)) return u const b = (urls.value || '').replace(/\/$/, '') const p = String(u).replace(/^\//, '') return b ? `${b}/${p}` : p } const avatarSrc = computed(() => { // ✅ 打码群:所有人头像统一替换(含自己) if (props.maskMode) return MASK_AVATAR_URL // typing/system 走 assistantAvatar if (props.role === 'typing' || props.role === 'system') return props.assistantAvatar || '' // 群聊:自己用 userAvatar(相对路径拼 urls),别人用 assistantAvatar(传进来的就是 full 或相对都可) if (isAssistantLike.value) return props.assistantAvatar || '' return fullUrl(props.userAvatar || '') }) // 解析内容 const parsed = computed(() => normalizeParsed(props.content)) const isTextPayload = computed(() => parsed.value && typeof parsed.value === 'object' && parsed.value.msgType === 'text') // ✅ 新增:location const isLocation = computed(() => parsed.value && typeof parsed.value === 'object' && parsed.value.msgType === 'location') const locationName = computed(() => String(parsed.value?.name || '位置')) const locationAddress = computed(() => String(parsed.value?.address || '')) const isFileLike = computed(() => { const o = parsed.value if (!o || typeof o !== 'object') return false if (o._kind === 'file') return true if (typeof o.type === 'number' && o.type >= 1 && o.type <= 4) return true return false }) const isForwardRecord = computed(() => { const o = parsed.value return o && typeof o === 'object' && (o.msgType === 'forward_record' || o.msgType === 'forward' || o .type === 5 || o.type === 6) }) const renderText = computed(() => { if (isTextPayload.value) return String(parsed.value?.text || '') if (parsed.value && typeof parsed.value === 'object' && typeof parsed.value.text === 'string' && parsed .value.text) { return String(parsed.value.text) } if (typeof props.content === 'string') return props.content return asString(props.content) }) const hasQuote = computed(() => { return !!(isTextPayload.value && parsed.value?.quote && (parsed.value.quote.preview || parsed.value.quote .senderName)) }) const quoteLine = computed(() => { const q = parsed.value?.quote || {} const name = q.senderName || '用户' const p = q.preview || '' return `${props.maskMode ? maskText(name) : name}:${p}` }) const fileType = computed(() => (isFileLike.value ? Number(parsed.value.type) : 0)) const fileUrl = computed(() => { if (!isFileLike.value) return '' const v = parsed.value if (v.playURL) return fullUrl(v.playURL) return fullUrl(v.fileName || '') }) const fileShowName = computed(() => (isFileLike.value ? parsed.value.showName || parsed.value.fileName || '' : '')) // 合并转发卡片数据 const forwardObj = computed(() => (isForwardRecord.value ? parsed.value : null)) const forwardTitle = computed(() => forwardObj.value?.title || '聊天记录') const forwardItems = computed(() => { const o = forwardObj.value || {} if (Array.isArray(o.forwardCastingChatArray)) return o.forwardCastingChatArray if (Array.isArray(o.forwardCastingChatGroupChatArray)) return o.forwardCastingChatGroupChatArray if (Array.isArray(o.items)) return o.items if (Array.isArray(o.chats)) return o.chats if (Array.isArray(o.records)) return o.records return [] }) const forwardCount = computed(() => Number(forwardObj.value?.count || forwardItems.value.length || 0)) function computePreviewText(raw) { if (!raw) return '' let s = raw if (typeof s !== 'string') { try { s = JSON.stringify(s) } catch { s = String(s) } } const obj = normalizeParsed(s) if (obj && typeof obj === 'object') { if (obj.msgType === 'text') return String(obj.text || '').slice(0, 60) if (obj.msgType === 'location') return `[位置] ${String(obj.name || obj.address || '').slice(0, 40)}` if (!obj.msgType && typeof obj.text === 'string' && obj.text) return String(obj.text).slice(0, 60) if (obj._kind === 'file' || (obj.type >= 1 && obj.type <= 4)) { const t = Number(obj.type || 0) const baseName = obj.showName || obj.fileName || '' if (t === 1) return baseName ? `[图片] ${baseName}` : '[图片]' if (t === 2) return baseName ? `[视频] ${baseName}` : '[视频]' if (t === 3) return baseName ? `[语音] ${baseName}` : '[语音]' if (t === 4) return baseName ? `[文件] ${baseName}` : '[文件]' return '[文件]' } if (obj.msgType === 'ad') return '[广告]' if (obj.type === 5 || obj.type === 6 || obj.msgType === 'forward' || obj.msgType === 'forward_record') { return obj.title || '[聊天记录]' } } return String(s).slice(0, 60) } const forwardLines = computed(() => { const arr = forwardItems.value.slice(0, 2).map(it => { const name = it.senderName || it.nickname || it.senderNickname || '用户' const n = props.maskMode ? maskText(name) : name return `${n}:${computePreviewText(it.content)}` }) return arr }) // 合并转发详情弹窗 const showForwardDetail = ref(false) function openForwardDetail() { showForwardDetail.value = true } function forwardSenderName(it) { const name = it?.senderName || it?.nickname || it?.senderNickname || '用户' return props.maskMode ? maskText(name) : name } function forwardContentObj(it) { return normalizeParsed(it?.content) } function forwardKind(it) { const o = forwardContentObj(it) if (o && typeof o === 'object') { if (o._kind === 'file' || o.type) return 'file' if (o.msgType === 'text') return 'text' if (o.msgType === 'location') return 'text' } return 'text' } function forwardPreview(it) { return computePreviewText(it?.content) } function forwardFileType(it) { const o = forwardContentObj(it) return Number(o?.type || 0) } function forwardFileUrl(it) { const o = forwardContentObj(it) const u = o?.playURL || o?.fileName || '' return fullUrl(u) } function forwardHasQuote(it) { const o = forwardContentObj(it) return !!(o && o.msgType === 'text' && o.quote && (o.quote.preview || o.quote.senderName)) } function forwardQuoteLine(it) { const o = forwardContentObj(it) || {} const q = o.quote || {} const name = q.senderName || '用户' return `${props.maskMode ? maskText(name) : name}:${q.preview || ''}` } // ✅ 转发详情里头像 function forwardAvatarUrl(it) { if (props.maskMode) return MASK_AVATAR_URL const u = it?.senderAvatar || it?.avatarUrl || it?.avatar || '' return fullUrl(u) || '/static/default/avatar.png' } function previewImage(val) { if (!val) return uni.previewImage({ current: val, urls: [val] }) } // ✅ 仅本人可删除/撤回 const aboveActions = computed(() => { return Theabovefunction.value.filter(a => a.title !== '删除' || isSelf.value) }) const belowActions = computed(() => { return Thefollowingfunctions.value.filter(a => a.title !== '撤回' || isSelf.value) }) function canRecallByRule() { // groupchat=true => 不限时撤回 const flag = uni.getStorageSync('groupchat') const unlimited = flag === true || flag === 'true' || flag === 1 || flag === '1' if (unlimited) return true // 否则 3 分钟内 const t = props.createTime ? new Date(props.createTime).getTime() : 0 if (!t) return true return Date.now() - t <= 3 * 60 * 1000 } // ✅ 顶部功能区:转发 / 收藏 / 删除 function Upperfunctionalarea(item) { const { title } = item if (title === '收藏') { if (props.forbidBookmark) { uni.showToast({ title: '本群已禁止收藏', icon: 'none' }) return } Iscollecting.value = true return } if (title === '转发') { if (props.forbidSpread) { uni.showToast({ title: '本群已禁止分享', icon: 'none' }) return } if (!props.castingChatId) { uni.showToast({ title: '该消息暂无法转发', icon: 'none' }) return } const isGroup = !!props.castingChatGroupId const contentStr = normalizeContentForForward() const payload = isGroup ? { source: 'groupChat', mode: 'single', castingChatGroupId: String(props.castingChatGroupId), castingChatGroupChatId: String(props.castingChatId), castingChatGroupChatIds: [String(props.castingChatId)], content: contentStr, items: [{ castingChatGroupChatId: String(props.castingChatId), senderId: String(props.userid || ''), senderName: displayNickname.value, senderAvatar: isSelf.value ? (props.userAvatar || '') : (props.assistantAvatar || ''), content: contentStr, createTime: props.createTime || '' }], mask: !!props.maskMode } : { source: 'chat', mode: 'single', castingChatId: String(props.castingChatId), castingChatIds: [String(props.castingChatId)], content: contentStr, items: [{ castingChatId: String(props.castingChatId), senderId: String(props.userid || ''), senderName: displayNickname.value, senderAvatar: isSelf.value ? (props.userAvatar || '') : (props.assistantAvatar || ''), content: contentStr, createTime: props.createTime || '' }] } close() uni.navigateTo({ url: '/pages/sociAlize/forward/Forwardfriend?payload=' + encodeURIComponent(JSON.stringify( payload)) }) return } if (title === '删除') { if (!isSelf.value) { uni.showToast({ title: '只能删除自己发送的消息', icon: 'none' }) return } if (!props.castingChatId) { uni.showToast({ title: '该消息暂无ID', icon: 'none' }) return } close() emit('delete', { id: String(props.castingChatId) }) return } } // ✅ 底部功能区:引入 / 多选 / 撤回 function Lowerfunctionalarea(item) { const { title } = item if (title === '引入') { close() emit('introduce', { content: props.content }) return } if (title === '多选') { if (!props.castingChatId) { uni.showToast({ title: '该消息暂无ID', icon: 'none' }) return } close() emit('multi-select', { id: String(props.castingChatId) }) return } if (title === '撤回') { if (!isSelf.value) { uni.showToast({ title: '只能撤回自己发送的消息', icon: 'none' }) return } if (!props.castingChatId) { uni.showToast({ title: '该消息暂无ID', icon: 'none' }) return } if (!canRecallByRule()) { uni.showToast({ title: '该消息已过三分钟无法撤回', icon: 'none' }) close() return } close() emit('recall', { id: String(props.castingChatId) }) return } } // 收藏消息 async function Collectmessages(isFileTransfer = false) { const consumerId = uni.getStorageSync('consumerId') if (!consumerId) return uni.showToast({ title: '请先登录', icon: 'none' }) if (!props.castingChatId) return uni.showToast({ title: '该消息暂无法收藏', icon: 'none' }) let option = {} const isGroup = !!props.castingChatGroupId if (isGroup) option.castingChatGroupChatId = String(props.castingChatId) else option.castingChatId = String(props.castingChatId) if (isFileTransfer) { option = { consumerId, castingSecret: !!isFileTransfer, fileTransfer: !!isFileTransfer } } else { option = { consumerId, fileTransfer: !!isFileTransfer, fileTransfer: !!isFileTransfer } } try { const res = await ajax.post({ url: 'castingChatCollect', method: 'post', data: option }) if (res?.data?.result === 'success') { uni.showToast({ title: '收藏成功', icon: 'none' }) close() } else { uni.showToast({ title: res?.data?.msg || '收藏失败', icon: 'none' }) } } catch (e) { uni.showToast({ title: '收藏异常', icon: 'none' }) } } // 复制 function copyContent() { if (props.forbidCopy) { uni.showToast({ title: '本群已禁止复制', icon: 'none' }) return } const text = isTextPayload.value ? renderText.value : asString(props.content || '') uni.setClipboardData({ data: text, success() { uni.showToast({ title: '已复制', icon: 'none' }) } }) } // 点击头像 function onAvatarClick() { // ✅ 打码群:不允许点头像跳转(避免泄露) if (props.maskMode) return if (!isGroupMate.value) return uni.navigateTo({ url: '/pages/sociAlize/stranger/Friendhomepage?uid=' + encodeURIComponent(JSON.stringify(props)) }) } // 图片点击:如果刚才是长按触发的弹窗,就不预览 function onImageClick() { if (preventImageClick.value) { preventImageClick.value = false return } previewImage(fileUrl.value) } // ✅ 新增:定位详情(复用你单聊定位详情页 positioning) function openLocationDetail() { const o = parsed.value || {} const payload = { name: o.name || '位置', address: o.address || '', latitude: Number(o.latitude || 0), longitude: Number(o.longitude || 0) } uni.navigateTo({ url: '/pages/sociAlize/sharedspace/positioning?payload=' + encodeURIComponent(JSON.stringify(payload)) }) } // 文件打开/下载 function openOrDownload(url) { if (!url) return // #ifdef H5 window.open(url, '_blank') // #endif // #ifndef H5 uni.downloadFile({ url, success: ({ tempFilePath }) => { if (uni.openDocument) { uni.openDocument({ filePath: tempFilePath }) } else { uni.showToast({ title: '已下载', icon: 'none' }) } }, fail: () => uni.showToast({ title: '下载失败', icon: 'none' }) }) // #endif } </script> <style scoped> .chat-message { display: flex; align-items: flex-start; margin-top: 24rpx; } .chat-message.user { flex-direction: row-reverse; } .avatar { width: 60rpx; height: 60rpx; border-radius: 50%; margin-right: 12rpx; } .chat-message.user .avatar { margin-left: 12rpx; margin-right: 0; } /* ✅ 昵称:对齐头像上方 */ .nickname-bar { align-self: flex-start; margin-bottom: 8rpx; font-size: 24rpx; color: #666; padding: 0 6rpx; max-width: 70vw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .chat-message.user .nickname-bar { align-self: flex-end; text-align: right; } .nickname-bar.clickable { color: #576b95; } .bubble-wrapper { display: flex; flex-direction: column; max-width: 75%; } .bubble { position: relative; padding: 16rpx 20rpx; line-height: 1.4; word-break: break-word; white-space: pre-wrap; } .chat-message.assistant .bubble { background: #fff; color: #000; border-radius: 16rpx 16rpx 16rpx 4rpx; } .chat-message.user .bubble { background: #95ec69; color: #fff; border-radius: 16rpx 16rpx 4rpx 16rpx; } .chat-message.assistant .bubble::before { content: ''; position: absolute; left: -8rpx; top: 20rpx; border-top: 8rpx solid transparent; border-right: 8rpx solid #fff; border-bottom: 8rpx solid transparent; } .chat-message.user .bubble::after { content: ''; position: absolute; right: -8rpx; top: 20rpx; border-top: 8rpx solid transparent; border-left: 8rpx solid #95ec69; border-bottom: 8rpx solid transparent; } .bubble.typing { display: flex; align-items: center; } .typing-text { color: #ccc; margin-right: 12rpx; font-size: 24rpx; } .dots { display: flex; width: 60rpx; justify-content: space-between; } .dot { width: 12rpx; height: 12rpx; background: #999; border-radius: 50%; animation: blink 1s infinite; } @keyframes blink { 0%, 100% { opacity: 0.2; } 50% { opacity: 1; } } .btn-copy { align-self: flex-end; margin-top: 6rpx; font-size: 20rpx; color: #999; } .badge { align-self: flex-end; margin-top: 6rpx; margin-left: auto; background: #fff; color: #fff; padding: 4rpx 8rpx; border-radius: 8rpx; font-size: 22rpx; } .quote-snippet { background: rgba(0, 0, 0, 0.06); border-radius: 10rpx; padding: 10rpx 12rpx; margin-bottom: 10rpx; } .quote-snippet-text { font-size: 24rpx; color: #333; } .forward-card { width: 540rpx; max-width: 70vw; border-radius: 12rpx; padding: 16rpx 18rpx; box-sizing: border-box; } .fc-title { font-size: 28rpx; color: #111; margin-bottom: 10rpx; font-weight: 600; } .fc-line { font-size: 26rpx; color: #333; margin-bottom: 6rpx; } .fc-count { margin-top: 10rpx; font-size: 26rpx; color: #666; } /* ✅ 新增:定位卡片(对齐单聊 Bubblechatbox 的 css) */ .loc-card { width: 520rpx; max-width: 70vw; border-radius: 12rpx; padding: 16rpx 18rpx; box-sizing: border-box; } .loc-name { font-size: 28rpx; color: #111; margin-bottom: 8rpx; font-weight: 600; } .loc-addr { font-size: 24rpx; color: #666; margin-bottom: 14rpx; } .loc-btn { height: 76rpx; border-radius: 10rpx; background: #d89833; color: #fff; display: flex; justify-content: center; align-items: center; } /* 弹窗整体 */ .action-popup { width: 686rpx; height: 342rpx; background-color: white; display: flex; justify-content: center; align-items: center; border-radius: 12rpx; } .action-grid { height: 50%; width: 90%; box-sizing: border-box; } .action-row { height: 50%; width: 100%; display: flex; justify-content: space-between; box-sizing: border-box; } .collect-row { height: 50%; width: 90%; box-sizing: border-box; display: flex; justify-content: space-between; align-items: center; padding: 0 20rpx; } /* ✅ 转发详情弹窗(优化) */ .forward-detail-popup { width: 686rpx; background: #fff; border-radius: 12rpx; padding: 18rpx; box-sizing: border-box; } .fd-row { width: 100%; display: flex; align-items: flex-start; margin-bottom: 18rpx; } .fd-avatar { width: 60rpx; height: 60rpx; border-radius: 50%; margin-right: 14rpx; background: #f2f2f2; } .fd-body { flex: 1; display: flex; flex-direction: column; max-width: 82%; } .fd-name { font-size: 24rpx; color: #666; margin-bottom: 6rpx; } .fd-bubble { background: #f4f4f4; border-radius: 12rpx; padding: 14rpx 16rpx; box-sizing: border-box; } .fd-text { font-size: 28rpx; color: #333; } .fd-quote { background: rgba(0, 0, 0, 0.06); border-radius: 10rpx; padding: 10rpx 12rpx; margin-bottom: 10rpx; } .fd-quote-text { font-size: 24rpx; color: #333; } .fd-file-line { font-size: 28rpx; color: #333; } </style>这个聊天长嗯只用引用和删除了 自己地方消息和别的消息都是一样删除删除的是本地自己的缓存不走服务器删除然后这个是usecontacts js文件的内容text<view @touchstart="handleTouchStart" @touchend="handleTouchEnd" class="bubble-wrapper"> <!-- ✅ 群聊昵称:强制显示(头像上方/气泡上方) --> <view v-if="showNicknameBar" class="nickname-bar" :class="{ clickable: isGroupMate && !maskMode }" @click="onAvatarClick"> {{ displayNickname }} </view> <!-- 气泡内容 --> <view class="bubble" :class="{ typing: role === 'typing' }"> <!-- 正在输入 --> <template v-if="role === 'typing'"> <view class="typing-text" @longpress="copyContent">{{ renderText }}</view> <view class="dots"> <view v-for="n in 3" :key="n" class="dot" :style="{ animationDelay: n * 0.2 + 's' }" /> </view> </template> <!-- ✅ 新增:定位卡片(对齐单聊 loc-card 样式) --> <template v-else-if="isLocation"> <view class="loc-card" @click.stop="openLocationDetail" @tap.stop="openLocationDetail"> <view class="loc-name">{{ locationName }}</view> <view class="loc-addr">{{ locationAddress }}</view> <view class="loc-btn">点击查看</view> </view> </template> <!-- 文件类 --> <template v-else-if="isFileLike"> <!-- 图片 --> <view v-if="fileType === 1"> <image :src="fileUrl" mode="widthFix" style="max-width: 480rpx; max-height: 560rpx; border-radius: 8rpx" @click="onImageClick" /> </view> <!-- 视频 --> <view v-else-if="fileType === 2" style="width: 520rpx; max-width: 75vw"> <!-- #ifdef APP-PLUS --> <MyVideo :videoUrl="fileUrl" /> <!-- #endif --> <!-- #ifdef MP-WEIXIN --> <video :src="fileUrl" controls style="width: 100%; border-radius: 8rpx" /> <!-- #endif --> </view> <!-- 语音 --> <view v-else-if="fileType === 3"> <audio :src="fileUrl" controls style="width: 100%" /> <view v-if="fileShowName" style="margin-top: 8rpx; color: #ddd; font-size: 24rpx"> {{ fileShowName }} </view> </view> <!-- 普通文件 --> <view v-else-if="fileType === 4" style="display: flex; align-items: center; gap: 12rpx"> <uni-icons type="paperclip" size="22" /> <text style="color: #000" @click="openOrDownload(fileUrl)">{{ fileShowName || '文件' }}</text> </view> </template> <!-- 合并转发:聊天记录卡片 --> <template v-else-if="isForwardRecord"> <view class="forward-card" @click="openForwardDetail"> <view class="fc-title">{{ forwardTitle }}</view> <view v-for="(line, i) in forwardLines" :key="i" class="fc-line">{{ line }}</view> <view class="fc-count">聊天记录({{ forwardCount }}条)</view> </view> </template> <!-- 文本 --> <template v-else> <view v-if="hasQuote" class="quote-snippet"> <text class="quote-snippet-text">{{ quoteLine }}</text> </view> <text class="content">{{ renderText }}</text> </template> </view> <!-- 复制按钮 --> <text v-if="showCopyButton && (role === 'assistant' || role === 'system')" class="btn-copy" @click="copyContent"> 点击复制 </text> <!-- 徽章 --> <text v-if="showBadge && (role === 'assistant' || role === 'system')" class="badge"> 以上内容由咖宠生成 </text> </view> </view> <!-- 长按弹窗 --> <up-popup :show="show" @close="close" mode="center" :closeable="true" :safeAreaInsetTop="true" closeIconPos="top-right" zIndex="99999" round="12" @open="open"> <view class="action-popup"> <!-- 第一层功能 --> <view v-if="!Iscollecting" class="action-grid"> <view class="action-row"> <view v-for="(item, index) of aboveActions" :key="'up-'+index" @click="Upperfunctionalarea(item)"> <up-button type="warning" style="height: 80rpx; width: 120rpx" :text="item.title" /> </view> </view> <view class="action-row"> <view v-for="(item, index) of belowActions" :key="'down-'+index" @click="Lowerfunctionalarea(item)"> <up-button type="warning" style="height: 80rpx; width: 120rpx" :text="item.title" /> </view> </view> </view> <!-- 收藏方式选择 --> <view v-else class="collect-row"> <up-button style="height: 80rpx; width: 186rpx" type="warning" @click="Collectmessages(false)" text="咖信收藏" /> <up-button style="height: 80rpx; width: 186rpx" type="warning" @click="Collectmessages(true)" text="咖密收藏" /> </view> </view> </up-popup> <!-- ✅ 合并转发详情弹窗:优化样式(头像+昵称+内容) --> <up-popup :show="showForwardDetail" @close="showForwardDetail=false" mode="center" :closeable="true" :safeAreaInsetTop="true" closeIconPos="top-right" zIndex="99998" round="12"> <view class="forward-detail-popup"> <scroll-view scroll-y style="height: 70vh"> <view v-for="(it, idx) in forwardItems" :key="idx" class="fd-row"> <image class="fd-avatar" :src="forwardAvatarUrl(it)" mode="aspectFill" /> <view class="fd-body"> <text class="fd-name">{{ forwardSenderName(it) }}</text> <view class="fd-bubble"> <view v-if="forwardHasQuote(it)" class="fd-quote"> <text class="fd-quote-text">{{ forwardQuoteLine(it) }}</text> </view> <template v-if="forwardKind(it) === 'file'"> <image v-if="forwardFileType(it) === 1" :src="forwardFileUrl(it)" mode="widthFix" style="max-width: 420rpx; border-radius: 8rpx" @click.stop="previewImage(forwardFileUrl(it))" /> <view v-else class="fd-file-line"> <text>{{ forwardPreview(it) }}</text> </view> </template> <template v-else> <text class="fd-text">{{ forwardPreview(it) }}</text> </template> </view> </view> </view> </scroll-view> </view> </up-popup> </view>
// @/store/usecontacts.js
import {
defineStore
} from 'pinia'
import {
ref,
reactive,
toRefs,
inject,
computed,
onMounted
} from 'vue'
import {
Getcontacts
} from '@/api/api.js'
import ajax from '@/api/ajax.js'
import {
pinyin as _p
} from 'pinyin-pro'
let pinyinFn = null
try {
pinyinFn = _p
} catch (e) {
pinyinFn = null
}
const LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
const FIXED_INDEX = Object.freeze(LETTERS.split('').concat('#'))
const OVERRIDE_INITIAL = new Map([])
let collatorZh = null
try {
collatorZh = new Intl.Collator('zh-Hans-u-co-pinyin', {
sensitivity: 'base',
numeric: true
})
} catch (e) {
try {
collatorZh = new Intl.Collator('zh', {
sensitivity: 'base',
numeric: true
})
} catch (e2) {
collatorZh = null
}
}
const isChineseChar = (ch) => {
if (!ch) return false
const code = ch.codePointAt(0)
return (code >= 0x4e00 && code <= 0x9fff) || (code >= 0x3400 && code <= 0x4dbf)
}
const ZH_ANCHOR_TABLE = [{
letter: 'A',
anchor: '阿'
}, {
letter: 'B',
anchor: '八'
}, {
letter: 'C',
anchor: '嚓'
},
{
letter: 'D',
anchor: '搭'
}, {
letter: 'E',
anchor: '蛾'
}, {
letter: 'F',
anchor: '发'
},
{
letter: 'G',
anchor: '噶'
}, {
letter: 'H',
anchor: '哈'
}, {
letter: 'I',
anchor: '衣'
},
{
letter: 'J',
anchor: '击'
}, {
letter: 'K',
anchor: '喀'
}, {
letter: 'L',
anchor: '垃'
},
{
letter: 'M',
anchor: '妈'
}, {
letter: 'N',
anchor: '拿'
}, {
letter: 'O',
anchor: '哦'
},
{
letter: 'P',
anchor: '啪'
}, {
letter: 'Q',
anchor: '期'
}, {
letter: 'R',
anchor: '然'
},
{
letter: 'S',
anchor: '撒'
}, {
letter: 'T',
anchor: '塌'
}, {
letter: 'U',
anchor: '乌'
},
{
letter: 'V',
anchor: '维'
}, {
letter: 'W',
anchor: '挖'
}, {
letter: 'X',
anchor: '昔'
},
{
letter: 'Y',
anchor: '压'
}, {
letter: 'Z',
anchor: '匝'
}
]
function zhCompareFallback(a = '', b = '') {
if (collatorZh) return collatorZh.compare(a, b)
try {
return a.localeCompare(b, 'zh')
} catch (e) {
return (a.codePointAt(0) || 0) - (b.codePointAt(0) || 0)
}
}
function initialFromZhAnchors(ch) {
let result = '#'
for (const {
letter,
anchor
}
of ZH_ANCHOR_TABLE) {
if (zhCompareFallback(ch, anchor) >= 0) result = letter
else break
}
return result
}
const SORT_CACHE = new Map()
function sortKey(name = '') {
const key = S:${name}
if (SORT_CACHE.has(key)) return SORT_CACHE.get(key)
let out = name
try {
if (pinyinFn) {
out = pinyinFn(name, {
toneType: 'none',
nonZh: 'consecutive'
})
.replace(/\s+/g, ' ')
.trim()
.toLowerCase()
}
} catch (e) {}
SORT_CACHE.set(key, out)
return out
}
const INITIAL_CACHE = new Map()
function getInitial(name) {
const str = String(name || '').trim()
if (!str) return '#'
const ch = str[0]
const cacheKey = ch
if (INITIAL_CACHE.has(cacheKey)) return INITIAL_CACHE.get(cacheKey)
textlet ret = '#' if (/[A-Za-z]/.test(ch)) { ret = ch.toUpperCase() } else if (/\d/.test(ch)) { ret = '#' } else if (isChineseChar(ch)) { if (OVERRIDE_INITIAL.has(ch)) { ret = OVERRIDE_INITIAL.get(ch) } else { try { if (pinyinFn) { const first = pinyinFn(ch, { pattern: 'first', toneType: 'none' }) || '' ret = (first[0] || '#').toUpperCase() } else { ret = initialFromZhAnchors(ch) } } catch (e) { ret = initialFromZhAnchors(ch) } } } else { ret = '#' } if (!LETTERS.includes(ret)) ret = '#' INITIAL_CACHE.set(cacheKey, ret) return ret
}
export const usecontacts = defineStore('usecontacts', () => {
const datasource = reactive({
indexList: [...FIXED_INDEX],
itemArr: [{
id: '1',
name: '张三',
nickname: '张三',
avatarUrl: '/static/demo/a.jpg'
},
{
id: '2',
name: '李强',
nickname: '李强',
avatarUrl: '/static/demo/b.jpg'
},
{
id: '3',
name: 'Zack',
nickname: 'Zack',
avatarUrl: '/static/demo/c.jpg'
}
],
newfriend: [{
id: 1,
name: '好友申请',
img: 'https://pic.rmb.bdstatic.com/bjh/240129/f3cac1943a140fa41d08985f9148daf65967.jpeg'
},
{
id: 2,
name: '文件传输助手',
img: 'https://pic.rmb.bdstatic.com/bjh/240129/5ac38e1aa31582572d7f90be2ba309437847.jpeg'
},
{
id: 3,
name: '咖密',
img: 'https://pic.rmb.bdstatic.com/bjh/240129/5ac38e1aa31582572d7f90be2ba309437847.jpeg'
},
{
id: 4,
name: '收藏',
img: 'https://pic.rmb.bdstatic.com/bjh/240129/5ac38e1aa31582572d7f90be2ba309437847.jpeg'
},
{
id: 5,
name: '群聊',
img: 'https://pic.rmb.bdstatic.com/bjh/240129/5ac38e1aa31582572d7f90be2ba309437847.jpeg'
},
{
id: 6,
name: '咖募',
img: 'https://pic.rmb.bdstatic.com/bjh/240129/240129/5ac38e1aa31582572d7f90be2ba309437847.jpeg'
},
{
id: 7,
name: '咖友',
img: 'https://pic.rmb.bdstatic.com/bjh/240129/5ac38e1aa31582572d7f90be2ba309437847.jpeg'
},
{
id: 8,
name: '咖信邮箱',
img: 'https://pic.rmb.bdstatic.com/bjh/240129/5ac38e1aa31582572d7f90be2ba309437847.jpeg'
},
{
id: 9,
name: '咖组',
img: 'https://pic.rmb.bdstatic.com/bjh/240129/5ac38e1aa31582572d7f90be2ba309437847.jpeg'
}
],
consumerId: '',
webProjectUrl: '',
keywords: '',
Selectgroupmembers: [],
GroupID: '',
GroupDetails: [],
Groupnickname: '',
Creategroupnickname: '',
navOffsetTop: 0,
isonline: false,
text// ✅ 新增:咖组相关(不影响旧字段) CastingTeamID: '', CastingTeamDetails: null }) const { indexList, itemArr, newfriend, consumerId, webProjectUrl, keywords, Selectgroupmembers, GroupID, GroupDetails, Groupnickname, Creategroupnickname, navOffsetTop, isonline, CastingTeamID, CastingTeamDetails } = toRefs(datasource) try { webProjectUrl.value = inject('webProjectUrl') || '' } catch (e) { webProjectUrl.value = '' } const groupedItemArr = computed(() => { const idxArr = Array.isArray(indexList.value) ? indexList.value : [...FIXED_INDEX] const items = Array.isArray(itemArr.value) ? itemArr.value : [] const map = {} idxArr.forEach((l) => (map[l] = [])) if (!map['#']) map['#'] = [] items.forEach((u) => { const displayName = (u?.nickname || u?.name || '').trim() const initial = getInitial(displayName) if (map[initial]) map[initial].push(u) else map['#'].push(u) }) idxArr.forEach((l) => { map[l].sort((a, b) => { const an = (a?.nickname || a?.name || '').trim() const bn = (b?.nickname || b?.name || '').trim() const ak = sortKey(an) const bk = sortKey(bn) if (ak !== bk) return ak < bk ? -1 : 1 return zhCompareFallback(an, bn) }) }) return idxArr.map((l) => map[l]) }) function dedupeByIdOrName(arr) { const seen = new Set() const out = [] for (const it of arr) { const key = String(it?.id ?? '') || `${(it?.name || '').trim()}|${(it?.nickname || '').trim()}` if (seen.has(key)) continue seen.add(key) out.push(it) } return out } function setItems(list = [], opt = { append: false, dedupe: true }) { const arr = Array.isArray(list) ? list : [] if (opt.append) { const merged = [...itemArr.value, ...arr] itemArr.value = opt.dedupe ? dedupeByIdOrName(merged) : merged } else { itemArr.value = opt.dedupe ? dedupeByIdOrName(arr) : arr } } const addContacts = (list = []) => setItems(list, { append: true, dedupe: true }) const addContact = (one) => { if (one) addContacts([one]) } function removeContacts(predicate) { if (typeof predicate === 'function') { itemArr.value = itemArr.value.filter((i) => !predicate(i)) } else if (Array.isArray(predicate)) { const set = new Set(predicate.map(String)) itemArr.value = itemArr.value.filter((i) => !set.has(String(i?.id))) } } function updateContact(match, patch) { const fn = typeof match === 'function' ? match : (i) => String(i?.id) === String(match) itemArr.value = itemArr.value.map((i) => { if (!fn(i)) return i const p = typeof patch === 'function' ? patch(i) : patch return { ...i, ...(p || {}) } }) } const optin = (val) => { uni.navigateTo({ url: `/pages/sociAlize/stranger/Friendhomepage?parameter=${encodeURIComponent(JSON.stringify(val))}` }) } async function getGetcontacts() { try { consumerId.value = uni.getStorageSync('consumerId') } catch (e) { consumerId.value = '' } const option = { consumerId: consumerId.value, keyword: '', rowStartIdx: 0, rowCount: '20' } try { const res = await Getcontacts(option) if (res && res.result === 'success') { setItems(res.friendArray, { append: false, dedupe: true }) indexList.value = [...FIXED_INDEX] } } catch (e) { indexList.value = [...FIXED_INDEX] } } /* ========================= 旧:建群(castingChatGroup)保持不变 ========================= */ const Creategroupchat = () => { const option = { id: '', title: keywords.value, notice: '', consumerIds: Selectgroupmembers.value, ownerId: consumerId.value, managerIds: '', cancelManagerIds: '' } return ajax .post({ url: 'castingChatGroup', data: option, method: 'post' }) .then((res) => { if (res.data.result === 'success') { Getgroupchatmessages(res.data.id) GroupID.value = res.data.id return res } }) .catch(() => { uni.showToast({ title: '建群失败', icon: 'none' }) }) } const Pagedisplay = (payload = {}) => { const option = { consumerId: payload.consumerId || '', performerId: payload.performerId || '' } return ajax.post({ url: 'consumerFollowPerformerShow', data: option, method: 'post' }) } const PublicgroupID = ref('') const Getgroupchatmessages = (val) => { const id = val const parameter = { castingChatGroupId: id, consumerId: uni.getStorageSync('consumerId') } PublicgroupID.value = id ajax .post({ url: 'castingChatGroupShow', data: parameter, method: 'post' }) .then((res) => { if (res.data.result === 'success') { GroupDetails.value = res.data Creategroupnickname.value = res.data.title } }) .catch(() => { uni.showToast({ icon: 'none' }) }) } const SelectgroupID = ref('') const donotdisturb = ref(false) const Topupvalu = ref(false) const Personalsettings = () => { const option = { castingChatGroupId: PublicgroupID.value, consumerId: consumerId.value, doNotDisturb: donotdisturb.value, retract: '', top: Topupvalu.value, save: '', nickname: '', showNickname: '' } ajax.post({ url: 'chatGroupSetting', data: option, method: 'post' }).then((res) => { if (res.data.result === 'success') { uni.showToast({ title: '设置成功', icon: 'none' }) } }) } function switch1Change(e) { donotdisturb.value = !!e?.detail?.value Personalsettings() } function Topup(e) { Topupvalu.value = !!e?.detail?.value Personalsettings() } const difference = (item) => { const { name } = item || {} if (name === '新的朋友' || name === '好友申请') { uni.navigateTo({ url: '/pages/sociAlize/stranger/Friendapplication' }) } else if (name === '群聊') { uni.navigateTo({ url: '/pages/sociAlize/stranger/managecrowd' }) } else if (name === '咖密') { const decrypt = true if (pas.value.defaultSecretPasswordType === 1) { uni.navigateTo({ url: '/pages/sociAlize/Coffee/setpassword/gesture' + '?decrypt=' + decrypt + '&mode=check' + '&passwordId=' + (pas.value.secretGesturePasswordId || '') + '&redirect=' + encodeURIComponent('/pages/sociAlize/Coffee/Coffeeability') }) } else if (pas.value.defaultSecretPasswordType === 2) { uni.navigateTo({ url: '/pages/sociAlize/Coffee/setpassword/Textpassword' + '?decrypt=' + decrypt + '&mode=check' + '&passwordId=' + (pas.value.secretStringPasswordId || '') + '&redirect=' + encodeURIComponent('/pages/sociAlize/Coffee/Coffeeability') }) } else { uni.navigateTo({ url: '/pages/sociAlize/Coffee/Coffeeability' }) } } else if (name === '文件传输助手') { const target = { id: 'file_helper', nickname: '文件传输助手', avatarUrl: '/static/file-helper.png' } uni.navigateTo({ url: '/pages/sociAlize/ChatDisplayPage/chat?target=' + encodeURIComponent(JSON .stringify(target)) }) } else if (name === '咖组') { uni.navigateTo({ url: '/pages/sociAlize/Coffeegroup/Coffeegroup' }) } } let pas = ref({ defaultSecretPasswordType: 1, secretGesturePasswordId: '013be19f-d5f3-4e6e-a116-b78b1f048a1a', secretStringPasswordId: 'bae33555-b2a9-44e9-8ede-9326c4874398' }) const Istherepassword = () => { let consumer = uni.getStorageSync('consumerId') try { ajax .post({ url: 'consumerSecretPasswordShow', data: { consumerId: consumer }, method: 'post' }) .then((res) => { if (res?.data.result === 'success') { pas.value.defaultSecretPasswordType = res.data.defaultSecretPasswordType pas.value.secretGesturePasswordId = res.data.secretGesturePasswordId pas.value.secretStringPasswordId = res.data.secretStringPasswordId } }) } catch (e) {} } const FriendDetails = (item) => { uni.navigateTo({ url: `/pages/sociAlize/stranger/Friendhomepage?parameter=${encodeURIComponent(JSON.stringify(item))}` }) } function Friendsonline() { try { ajax .post({ url: 'counsumerOnline', data: { consumerId: uni.getStorageSync('consumerId') }, method: 'post' }) .then((res) => { if (res.data.result === 'success') isonline.value = res.data.online }) .catch(() => {}) } catch (err) {} } function setNavOffsetTop(navBarHeight = 0) { try { const sys = uni.getSystemInfoSync() const statusBarH = sys?.statusBarHeight || 0 navOffsetTop.value = statusBarH + (Number(navBarHeight) || 0) } catch (e) { navOffsetTop.value = Number(navBarHeight) || 0 } } let refreshTimer = null function refreshIndexLayoutFromStore(reason = '') { clearTimeout(refreshTimer) refreshTimer = setTimeout(() => { // uni.$emit('contacts-refresh', reason) }, 60) } /* ========================= ✅ 新增:咖组 castingTeam(不影响旧方法) ========================= */ // payload: { id, parentId, title, notice, consumerIds, ownerId ... } const CreateCastingTeam = (payload = {}) => { // 新建时 ownerId 必传(你接口写的),这里自动用当前用户 let myId = consumerId.value try { if (!myId) myId = uni.getStorageSync('consumerId') || '' } catch (e) {} const option = { id: payload.id || '', parentId: payload.parentId || '', title: payload.title || '', notice: payload.notice || '', consumerIds: payload.consumerIds || [], ownerId: payload.ownerId || myId, cancelOwnerId: payload.cancelOwnerId || '', managerIds: payload.managerIds || '', cancelManagerIds: payload.cancelManagerIds || '' } console.log(option,'里面的创建参数') return ajax.post({ url: 'castingTeam', data: option, method: 'post' }) .then((res) => { console.log(res,'大组里面的数据') return res if (res?.data?.result === 'success' && res?.data?.id) { CastingTeamID.value = res.data.id } }) .catch(() => { uni.showToast({ title: '咖组创建失败', icon: 'none' }) }) } onMounted(() => { getGetcontacts() Istherepassword() }) return { indexList, itemArr, newfriend, consumerId, webProjectUrl, keywords, Selectgroupmembers, GroupID, GroupDetails, Groupnickname, Creategroupnickname, navOffsetTop, isonline, optin, difference, getGetcontacts, Creategroupchat, SelectgroupID, Personalsettings, switch1Change, Topup, Getgroupchatmessages, FriendDetails, Friendsonline, Pagedisplay, groupedItemArr, getInitial, setNavOffsetTop, setItems, addContacts, addContact, removeContacts, updateContact, PublicgroupID, refreshIndexLayoutFromStore, pas, // ✅ 新增导出 CreateCastingTeam, CastingTeamID, CastingTeamDetails }
})
这是后端所有的api 现有的api了 咖组
1.castingTeamChat咖组聊天
入口参数
String consumerId, 用户id
String castingTeamId,组id
String content, 内容
Integer type 类型
teamFile上传文件名
返回值
id
2.castingTeamChatListByCastingTeam组内聊天列表
入口参数
String castingTeamId, 组id Integer rowStartIdx, 起始行 Integer rowCount 行数
返回值
castingTeamChatArray 组聊天列表
id
content内容
type类型
fileName 文件名
showName 显示名
playURL 阿里云地址
createTime 建立时间
senderId 发送者id
3.searchCastingTeamChatList查询组聊天列表
入口参数
String consumerId,人id
String keyword 关键字
Integer rowStartIdx, 起始行
Integer rowCount 行数
返回值
castingTeamChatArray 组聊天列表
id
castingTeamId组id
castingTeamTitle组标题
senderId发送者id
senderNickname 发送者昵称
senderAvatarUrl 发送者头像
content内容
type类型
fileName 文件名
showName 显示名
playURL 阿里云地址
createTime 建立时间
senderId 发送者id
4.clockIn 打卡
入口参数
String id,
String consumerId, 用户id
String castingTeamId, 组id
String address, 地址
Date clockTime, 打卡时间
String longitude经度,
String latitude, 纬度
Integer type, 类型(0,正常上班 1,外勤上班2,正常下班3,外勤下班)
String remark备注
5.clockInList打卡列表
入口参数
String consumerId, 用户id
String castingTeamId, 组id
Integer rowStartIdx,起始行
Integer rowCount行数
返回值
clockInArray打卡列表
id
clockTime打卡时间
address地址
remark备注
latitude经度
longitude纬度
type类型
6.deleteCastingTeam 解散咖组
入口参数
String castingTeamId,群id
String consumerId, 用户id
7.deleteCastingTeamConsumer 删除组员、退出组
入口参数
String castingTeamId, 组id
String consumerId,用户id
8.castingTeamList 人的咖组列表
入口参数
String consumerId, 用户id
String ketword, 关键字
Integer rowStartIdx,起始行
Integer rowCount行数
String parentId 父组id
text返回值 castingTeamConsumerObject 组列表 avatarFileArray, 组员头像列表 castingTeamId, 组id castingTeamTitle, 组名称 castingTeamNotice, 组公告 castingTeamJoinlock, 进组方式 castingTeamMute, 是否禁言 castingTeamLock, 是否锁定该组
manager 是否为管理员
owner是否为组拥有者
doNotDisturb 消息免打扰
top 是否置顶
nickname 在组里的昵称
parentId 父组id
count 组内人数
lastChatContent 组里最后一条消息
lastChatSenderId 最后一条消息发送人id
9.castingTeam 组维护、加入、新建
入口参数
String id, 修改用
String parentId, 父组id
String title, 名称
String notice, 公告
String[] consumerIds, 加入组的用户
String ownerId,所有者id。新建时用,更换所有者时候用
String cancelOwnerId,取消管理员id,变更管理员时用
String[] managerIds,管理员id,变更管理员时用
String[] cancelManagerIds, 取消管理员id,变更管理员时用
返回值
id 组id
10.castingTeamAdminSetting 管理员、拥有者对组的设置
入口参数
String castingTeamId, 组id
String consumerId, 用户id
Boolean joinLock, 进群方式
Boolean mute, 全体禁言
Boolean lock, 锁定该组
String gesturePasswordId, 手势密码id
String stringPasswordId, 文字密码id
Integer defaultSecretPasswordType, 密码类型
11.castingTeamSetting 个人在咖组中的设置
入口参数
String castingTeamId, 组id
String consumerId, 用户id
Boolean doNotDisturb, 免扰
Boolean top, 置顶
String nickname, 群昵称
12.castingTeamFile组文件内文件上传
入口参数
teamFile 文件名
返回值
id 文件id
13.deleteCastingTeamFile删除组文件内文件
入口参数
id
14.castingTeamFileGroup组文件维护
入口参数
String id,
String consumerId, 用户id
String castingTeamId, 组id
Date startTime,开始时间
Date overTime, 结束时间
String title, 标题
Integer type,类型(0 组训,1 通告, 2 日志, 3 周报, 4 帐,5 课, 6 工作文档, 7 阅读文档)
String content, 内容
String[] castingTeamFileIds上传文件的id数组
15.castingTeamFileGroupList组文件列表
入口参数
String consumerId, 用户id
String castingTeamId, 组id
Integer type,类型
Integer rowStartIdx,起始行
Integer rowCount行数
返回值
castingTeamFileGroupArray组文件数组
id
consumerId用户id
content内容
startTime开始时间
overTime结束时间
title标题
type类型
16.webSocket长链接接口
/ws/{consumerId}建立链接:例如wss://www.doufan.net/ws/xxxxxxxxxxxxx
{consumerId}为当前用户的id
接受message格式:
{“type”:”teamChat”,”id”:”xxxxxxxx”}json转成的字符串格式 群内聊天
接受到正确的格式message后会向聊天双方或发送消息,消息格式:
[
content:“XXX” 聊天内容
type:0(0 文字 1 图片 2 视频 3 声音4 文件) fileName:“xxx”
showName:“xxx”
playURL:“xxx”
senderId:"xxx" 发送者
createTime: 建立时间
]
现在这个 组聊天的websockets 已经出来了 这个是组的16.webSocket长链接接口
/ws/{consumerId}建立链接:例如wss://www.doufan.net/ws/xxxxxxxxxxxxx
{consumerId}为当前用户的id
接受message格式:
{“type”:”teamChat”,”id”:”xxxxxxxx”}json转成的字符串格式 群内聊天
接受到正确的格式message后会向聊天双方或发送消息,消息格式:
[
content:“XXX” 聊天内容
type:0(0 文字 1 图片 2 视频 3 声音4 文件) fileName:“xxx”
showName:“xxx”
playURL:“xxx”
senderId:"xxx" 发送者
createTime: 建立时间
]
这个是WsClient // /utils/WsClient.js
// 统一 WebSocket 客户端(uni-app)。
// - 自动重连(2s)
// - 心跳(默认 15s,可配置)
// - 仅在本页面使用时保持连接,离开页面请调用 close()
// - 始终向上层回调“字符串”消息(便于页面 JSON.parse)
// - 过滤 "PONG/pong/OK/CONNECT_SUCCESS" 等探活/握手字符串
export default class WsClient {
constructor(
url, {
heartbeat = true,
heartbeatInterval = 15000,
protocols = undefined,
autoReconnect = true,
reconnectDelay = 2000,
} = {}
) {
this.url = url;
this.protocols = protocols;
this.autoReconnect = autoReconnect;
this.reconnectDelay = reconnectDelay;
textthis.socketTask = null; this.isOpen = false; this.onMessageCallback = null; this.onOpenCallback = null; this.onCloseCallback = null; this.onErrorCallback = null; this.heartbeat = heartbeat; this.heartbeatInterval = heartbeatInterval; this._hbTimer = null; this._connectedNotified = false; // 仅真正建立连接时对外通知一次 this._manuallyClosed = false; // 用于区分主动关闭与被动断线 this._connect(); } onOpen(cb) { this.onOpenCallback = cb; } onClose(cb) { this.onCloseCallback = cb; } onError(cb) { this.onErrorCallback = cb; } getMessage(cb) { this.onMessageCallback = cb; } send(data) { if (!this.isOpen || !this.socketTask) return; let payload = data; if ( data != null && typeof data !== "string" && !(typeof ArrayBuffer !== "undefined" && data instanceof ArrayBuffer) ) { try { payload = JSON.stringify(data); } catch (_) {} } try { this.socketTask.send({ data: payload }); } catch (_) {} } close(code = 1000, reason = "client-close") { this._manuallyClosed = true; this._clearHeartbeat(); try { this.socketTask && this.socketTask.close({ code, reason }); } catch (_) {} this.socketTask = null; this.isOpen = false; this._connectedNotified = false; } // === 内部实现 === _connect() { try { this.socketTask = uni.connectSocket({ url: this.url, protocols: this.protocols, success: () => {}, fail: () => { if (this.autoReconnect && !this._manuallyClosed) { setTimeout(() => this._connect(), this.reconnectDelay); } }, }); } catch (e) { if (this.autoReconnect && !this._manuallyClosed) { setTimeout(() => this._connect(), this.reconnectDelay); } return; } this.socketTask.onOpen(() => { this.isOpen = true; this._manuallyClosed = false; if (this.heartbeat) this._startHeartbeat(); if (!this._connectedNotified) { this._connectedNotified = true; typeof this.onOpenCallback === "function" && this.onOpenCallback(); } }); this.socketTask.onMessage((res) => { console.log(res, '收到消息了') let payload = res?.data; // 兼容 ArrayBuffer -> 转为字符串 if (typeof ArrayBuffer !== "undefined" && payload instanceof ArrayBuffer) { try { payload = new TextDecoder("utf-8").decode(payload); } catch (_) { payload = ""; } } // 过滤心跳/握手字符串 if (typeof payload === "string") { const s = payload.trim(); if (!s) return; if (s === "CONNECT_SUCCESS" || s === "PONG" || s === "pong" || s === "OK") { return; // 忽略探活/握手 } // 统一以“字符串”上抛(方便页面 JSON.parse) typeof this.onMessageCallback === "function" && this.onMessageCallback(s); return; } // 极少数平台可能直接给对象,这里兜底转成字符串再抛 try { const s = JSON.stringify(payload); typeof this.onMessageCallback === "function" && this.onMessageCallback(s); } catch (_) { // 退一步直接抛对象 typeof this.onMessageCallback === "function" && this.onMessageCallback(payload); } }); this.socketTask.onClose((evt) => { this.isOpen = false; this._clearHeartbeat(); typeof this.onCloseCallback === "function" && this.onCloseCallback(evt); this.socketTask = null; if (this.autoReconnect && !this._manuallyClosed) { setTimeout(() => this._connect(), this.reconnectDelay); } }); this.socketTask.onError((err) => { this.isOpen = false; this._clearHeartbeat(); typeof this.onErrorCallback === "function" && this.onErrorCallback(err); this.socketTask = null; if (this.autoReconnect && !this._manuallyClosed) { setTimeout(() => this._connect(), this.reconnectDelay); } }); } _startHeartbeat() { this._clearHeartbeat(); this._hbTimer = setInterval(() => { if (!this.isOpen || !this.socketTask) return; try { this.socketTask.send({ data: JSON.stringify({ type: "ping" }) }); } catch (_) {} }, this.heartbeatInterval); } _clearHeartbeat() { clearInterval(this._hbTimer); this._hbTimer = null; }
}是组和群公用的一个api 文件 可以是实现聊天内容 这个CafeGroupDetails 不写完整的页面修正下两个带参跳转方法就行 写一个完整的下面是这个页面的内容Groupchat 这个组聊天页面
配上16.webSocket长链接接口
/ws/{consumerId}建立链接:例如wss://www.doufan.net/ws/xxxxxxxxxxxxx
{consumerId}为当前用户的id
接受message格式:
{“type”:”teamChat”,”id”:”xxxxxxxx”}json转成的字符串格式 群内聊天
接受到正确的格式message后会向聊天双方或发送消息,消息格式:
[
content:“XXX” 聊天内容
type:0(0 文字 1 图片 2 视频 3 声音4 文件) fileName:“xxx”
showName:“xxx”
playURL:“xxx”
senderId:"xxx" 发送者
createTime: 建立时间
]
实现聊天功能 下一个是 GroupDetails 页面 根据图片的权利来区别第一张是
普通的组员看到的 第二张就是全的功能了只有群主和管理员可以 严格根据图片 剩下的功能api 已经可以用到的功能就要把他实现了 api 没写的先放哪里
比如就是组详情这个人员api就没有 所以这个GroupDetails页面一定要写最完整的页面所有的功能一个不少下一个就是完整的组聊天的气泡组件groupchatbubble组件页面的内容了 下一个就是
最完整的usecontacts js 内容了 写新的功能的同时也要全部保留之前的旧的功能 在原有的基础上 新增功能 一定要返回最完整的usecontacts js 内容了
然后这个是公用的WsClient js 文件 如果可以公用那就 原封不动 如果不能公用那就修改一下内容 我在新加一个js 文件 一定不能让新的影响到旧的 打通我的所有的页面需求写出最完整的页面和文件 一定要严格根据图片样式修改组详情这个页面
群组解散大组就是所有的都没了包括小组 不能解散小组就是小组没了 有其他的小组还能发送消息 解散的小组没了可以进入但是发送不了消息 发送就提示该组已经解散 解散大组 大组也发送不了