这个是CafeGroupDetails页面的内容<template> <view :class="...

Criado em: 8 de janeiro de 2026

Respondido usando GPT-5.2 Thinking por Chat01

Pergunta

这个是CafeGroupDetails页面的内容<template>
<view :class="['app-container', theme]">
<view class="content">
<view class="overlay">
<!-- 固定 Header:滑动不移动 -->
<view class="fixed-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" />
</view>

text
<view class="center-box"> <text class="title-text">{{ pageTitle }}</text> </view> <view @click="EnteGroupSettings" class="right-box"> <uni-icons type="more-filled" color="#000000" size="22" /> </view> </view> </view> <!-- 滚动区域:顶部预留 header 高度 --> <scroll-view scroll-y class="scroll-area" :style="{ paddingTop: headerOffset + 'px' }" :lower-threshold="80" @scrolltolower="handleScrollToLower"> <view class="wrap"> <!-- 顶部:大组卡片 + 组中组 --> <view class="card"> <!-- 大组行:头像 + 组名(人数) + 时间 --> <view @click="largegroupchat()" style="" class="group-row"> <view class="group-left"> <view class="avatar-box"> <!-- 1 --> <view v-if="team?.avatarFileArray && team.avatarFileArray.length === 1" class="avatar-one"> <image class="img-fill" :src="fullImg(team.avatarFileArray[0])" mode="aspectFill" /> </view> <!-- 2 --> <view v-else-if="team?.avatarFileArray && team.avatarFileArray.length === 2" class="avatar-two"> <image v-for="(img, i) in team.avatarFileArray" :key="i" class="img-two" :src="fullImg(img)" mode="aspectFill" /> </view> <!-- 3 --> <view v-else-if="team?.avatarFileArray && team.avatarFileArray.length === 3" class="avatar-three"> <image v-if="team.avatarFileArray[0]" :src="fullImg(team.avatarFileArray[0])" class="img-three a" mode="aspectFill" /> <image v-if="team.avatarFileArray[1]" :src="fullImg(team.avatarFileArray[1])" class="img-three b" mode="aspectFill" /> <image v-if="team.avatarFileArray[2]" :src="fullImg(team.avatarFileArray[2])" class="img-three c" mode="aspectFill" /> </view> <!-- 4 --> <view v-else-if="team?.avatarFileArray && team.avatarFileArray.length >= 4" class="avatar-four"> <image v-for="(img, i) in team.avatarFileArray.slice(0, 4)" :key="i" class="img-four" :src="fullImg(img)" mode="aspectFill" /> </view> <!-- fallback --> <view v-else class="avatar-one"> <image class="img-fill" :src="fullImg(team?.img || '')" mode="aspectFill" /> </view> <!-- 列表来的 --> <view v-if="isGroupList" class="avatar-one"> <image class="img-fill" :src="fullImg(team?.senderAvatarUrl || '')" mode="aspectFill" /> </view> </view> <!-- ✅ 组名(人数)严格按图 --> <view class="group-title-wrap"> <text class="group-title">{{ teamTitleShow }}</text> <view class="notice-row"> <text class="notice-text">{{ teamLastChatShow }}</text> </view> </view> </view> <view class="group-right"> <text class="time-text">{{ nowHHMM }}</text> </view> </view> <view class="divider" /> <!-- 组中组(小组) --> <view class="subcard"> <view class="subcard-header"> <text class="subcard-title">组中组</text> <!-- ✅ 只点右侧箭头折叠(避免整行点击误触) --> <view class="arrow-hit" @click.stop="togglePanel('subTeams')"> <uni-icons :type="panel.subTeams ? 'top' : 'bottom'" color="#666666" size="18" /> </view> </view> <view v-show="panel.subTeams"> <view v-if="subLoading" class="sub-loading">加载中...</view> <view v-else-if="!subTeams.length" class="sub-empty">暂无小组</view> <!-- 标题(人数) + 时间;下方 lastChatContent --> <view style="" v-for="(item, idx) in subTeams" :key="item.castingTeamId || idx" class="subteam-row" @click="enterSubTeam(item)"> <view class="subteam-left"> <image class="subteam-avatar" :src="fullImg(item.img || (item.avatarFileArray && item.avatarFileArray[0]) || '')" mode="aspectFill" /> <view class="subteam-text"> <text class="subteam-title">{{ subTeamTitleShow(item) }}</text> <text class="subteam-sub">{{ subTeamLastChatShow(item) }}</text> </view> </view> <view class="subteam-right"> <text class="time-text">{{ nowHHMM }}</text> </view> </view> <!-- 小组加载状态 --> <view v-if="subTeams.length" class="list-footer"> <text v-if="subLoadingMore" class="footer-text">加载中...</text> <text v-else-if="subFinished" class="footer-text">已经到底啦~</text> </view> </view> </view> </view> <!-- 工作流 --> <view class="card"> <view class="section-header"> <text class="section-title">工作流</text> <view class="arrow-hit" @click.stop="togglePanel('workflow')"> <uni-icons :type="panel.workflow ? 'top' : 'bottom'" color="#666666" size="18" /> </view> </view> <view v-show="panel.workflow" class="grid"> <view v-for="(it, i) in workflowItems" :key="i" class="grid-item" @click.stop="onToolClick(it,i)"> <view :class="['icon-box', it.active ? 'icon-active' : '']"> <uni-icons :type="it.icon" :color="it.active ? '#ffffff' : '#333333'" size="26" /> </view> <text class="grid-text">{{ it.name }}</text> </view> </view> </view> <!-- 信息流 --> <view class="card"> <view class="section-header"> <text class="section-title">信息流</text> <view class="arrow-hit" @click.stop="togglePanel('info')"> <uni-icons :type="panel.info ? 'top' : 'bottom'" color="#666666" size="18" /> </view> </view> <view v-show="panel.info" class="grid"> <view v-for="(it, i) in infoItems" :key="i" class="grid-item" @click.stop="onToolClick(it)"> <view class="icon-box"> <uni-icons :type="it.icon" color="#333333" size="26" /> </view> <text class="grid-text">{{ it.name }}</text> </view> </view> </view> <!-- 应用 --> <view class="card"> <view class="section-header"> <text class="section-title">应用</text> <view class="arrow-hit" @click.stop="togglePanel('apps')"> <uni-icons :type="panel.apps ? 'top' : 'bottom'" color="#666666" size="18" /> </view> </view> <view v-show="panel.apps" class="grid"> <view v-for="(it, i) in appItems" :key="i" class="grid-item" @click.stop="onToolClick(it,i)"> <view class="icon-box"> <uni-icons :type="it.icon" color="#333333" size="26" /> </view> <text class="grid-text">{{ it.name }}</text> </view> </view> </view> <view style="height: 60rpx;" /> </view> </scroll-view> </view> </view> </view>
</template> <script setup> import ajax from '@/api/ajax.js' import { getStatusBarHeight, getTitleBarHeight } from '@/utils/system.js' import { ref, computed, inject, reactive } from 'vue' import { storeToRefs } from 'pinia' import { useMainStore } from '@/store/index.js' import { onLoad } from '@dcloudio/uni-app' // theme const store = useMainStore() const { theme } = storeToRefs(store) // header heights const statusBarHeight = ref(getStatusBarHeight()) const headerBarHeight = ref(getTitleBarHeight()) const headerOffset = computed(() => Number(statusBarHeight.value || 0) + Number(headerBarHeight.value || 0)) // img prefix const injectedWebProjectUrl = inject('webProjectUrl', '') const webProjectUrl = computed(() => injectedWebProjectUrl || getApp().globalData?.webProjectUrl || '') function fullImg(path = '') { const p = String(path || '').trim() if (!p) return '' if (/^https?:\/\//i.test(p)) return p return (webProjectUrl.value || '') + p } // params const from = ref('') const team = ref(null) // computed title const pageTitle = computed(() => team.value?.castingTeamTitle || '咖组详情') // 人数 const teamTitleShow = computed(() => { const t = team.value?.castingTeamTitle || '咖组' const count = Number(team.value?.count || 0) return count ? `${t}(${count})` : t }) // 组设置 function EnteGroupSettings() { uni.navigateTo({ url: '/pages/sociAlize/Coffeegroup/Createcoffeegroup/GroupSettings?teamId=' + team.value.castingTeamId }) } // 最后一条消息 const teamLastChatShow = computed(() => { const t = team.value const txt = t?.lastChatContent || t?.castingTeamNotice || '内容' return String(txt || '内容') }) // time const nowHHMM = computed(() => { const d = new Date() const hh = String(d.getHours()).padStart(2, '0') const mm = String(d.getMinutes()).padStart(2, '0') return `${hh}:${mm}` // 你的图里是“10:01” }) // back const navigateBack = () => uni.navigateBack() // ✅ 面板开关:用 reactive,保证 v-show 一定刷新 const panel = reactive({ subTeams: true, workflow: true, info: true, apps: true }) function togglePanel(key) { panel[key] = !panel[key] // ✅ 只有展开组中组时,才触发请求(没加载过) if (key === 'subTeams' && panel.subTeams && !subTeams.value.length) { fetchSubTeams(false) } } // ---------- subTeams: castingTeamList(parentId) ---------- const subTeams = ref([]) const subLoading = ref(false) const subLoadingMore = ref(false) const subFinished = ref(false) const pageSize = 20 const ketword = ref('') function respData(res) { return res?.data || res || {} } function isSuccess(res) { const d = respData(res) const r = d?.result if (r === 'success') return true if (r === true) return true if (r === 0 || r === '0') return true if (d?.code === 0) return true if (d?.success === true) return true return false } function respMsg(res) { const d = respData(res) return d?.remark || d?.msg || d?.message || d?.error || '' } // 组训 function fetchSubTeams(isLoadMore = false) { if (!team.value?.castingTeamId) return if (subFinished.value && isLoadMore) return if (isLoadMore) { if (subLoadingMore.value) return subLoadingMore.value = true } else { subLoading.value = true subFinished.value = false } const option = { consumerId: uni.getStorageSync('consumerId'), ketword: ketword.value || '', rowStartIdx: isLoadMore ? subTeams.value.length : 0, rowCount: pageSize, parentId: String(team.value.castingTeamId) } ajax.post({ url: 'castingTeamList', data: option, method: 'post' }) .then(res => { const d = respData(res) if (!isSuccess(res)) { uni.showToast({ title: respMsg(res) || '获取小组失败', icon: 'none' }) return } console.log(res, '里面的获取群详情人数') const list = d?.castingTeamConsumerObject || d?.castingTeamConsumerObjectArray || d?.castingTeamConsumerArray || d?.castingTeamArray || d?.data?.castingTeamConsumerObject || d?.data?.castingTeamConsumerObjectArray || [] const arr = Array.isArray(list) ? list : [] subTeams.value = isLoadMore ? subTeams.value.concat(arr) : arr if (!arr.length || arr.length < pageSize) subFinished.value = true }) .catch(() => { uni.showToast({ title: '获取小组失败', icon: 'none' }) }) .finally(() => { subLoading.value = false subLoadingMore.value = false }) } // 大组跳转 function largegroupchat() { if (!team.value) return uni.navigateTo({ url: '/pages/sociAlize/Coffeegroup/Groupchat/Groupchat' + '?from=largegroup' + '&father=' + encodeURIComponent(JSON.stringify(team.value)) }) } function handleScrollToLower() { // 只在组中组展开时触发小组分页 if (!panel.subTeams) return if (!subLoading.value && !subLoadingMore.value && !subFinished.value) { fetchSubTeams(true) } } // ✅ 小组标题:组名(count) function subTeamTitleShow(item) { const t = item?.castingTeamTitle || '小组' const count = Number(item?.count || 0) return count ? `${t}(${count})` : t } // ✅ 小组最后消息:lastChatContent 优先 function subTeamLastChatShow(item) { const txt = item?.lastChatContent || item?.castingTeamNotice || '内容' return String(txt || '内容') } // 跳转小组 function enterSubTeam(item) { console.log(item, '里面的参数') if (!item) return uni.navigateTo({ url: '/pages/sociAlize/Coffeegroup/Groupchat/Groupchat' + '?from=subTeam' + '&team=' + encodeURIComponent(JSON.stringify(item)) }) } // ---------- items ---------- const workflowItems = ref([{ name: '组讯', icon: 'list', active: false }, { name: '通告', icon: 'notification', active: false }, { name: '工作日志', icon: 'compose', active: false }, { name: '周报', icon: 'paperplane', active: false }, { name: '流水账', icon: 'wallet', active: false } ]) const infoItems = ref([{ name: '视频会议', icon: 'videocam' }, { name: '直播', icon: 'videocam-filled' }, { name: '咖课', icon: 'mic' }, { name: '工作文档', icon: 'folder' }, { name: '围读文档', icon: 'paperclip' } ]) const appItems = ref([{ name: '日历', icon: 'calendar' }, { name: '天气', icon: 'cloud' }, { name: '咖信邮箱', icon: 'email' }, { name: '咖募', icon: 'personadd' }, { name: '咖友', icon: 'staff' }, { name: '咖安', icon: 'locked' }, { name: '地图', icon: 'map' }, { name: '打卡', icon: 'flag' }, { name: '相册', icon: 'image' } ]) function onToolClick(it, i) { if (!it) return const { name } = it const base = '/pages/sociAlize/Coffeegroup/workflow' const trainStr = encodeURIComponent(JSON.stringify(team.value)) // 组训 / 通告(你原来就有) if (name === '组讯') { uni.navigateTo({ url: `${base}/organizetrain/organizetrain?type=0&title=${encodeURIComponent('组训')}&train=${trainStr}` }) return } if (name === '通告') { uni.navigateTo({ url: `${base}/organizetrain/organizetrain?type=1&title=${encodeURIComponent('通告')}&train=${trainStr}` }) return } if (name === '工作日志') { uni.navigateTo({ url: '/pages/sociAlize/Coffeegroup/workflow/worklog/Logpage?from=organize' + '&type=2' + '&train=' + encodeURIComponent(JSON.stringify(team.value)) }) return } if (name === '周报') { uni.navigateTo({ url: '/pages/sociAlize/Coffeegroup/workflow/worklog/Logpage?from=organize' + '&type=3' + '&train=' + encodeURIComponent(JSON.stringify(team.value)) }) return } if (name === '流水账') { uni.navigateTo({ url: '/pages/sociAlize/Coffeegroup/workflow/worklog/Logpage?from=organize' + '&type=4' + '&train=' + encodeURIComponent(JSON.stringify(team.value)) }) return } if (name === '工作文档') { uni.navigateTo({ url: '/pages/sociAlize/Coffeegroup/information/workdocument' + `?type=6&title=${encodeURIComponent('工作文档')}` + `&train=${encodeURIComponent(JSON.stringify(team.value))}` }) return } if (name === '围读文档') { uni.navigateTo({ url: '/pages/sociAlize/Coffeegroup/information/workdocument' + `?type=7&title=${encodeURIComponent('围读文档')}` + `&train=${encodeURIComponent(JSON.stringify(team.value))}` }) return } if (name === '日历') { uni.navigateTo({ url: '/pages/sociAlize/Coffeegroup/application/calendar/calendar' }) return } if (name === '天气') { uni.navigateTo({ url: '/pages/sociAlize/Coffeegroup/application/weather/weather' }) return } if (name === '咖课') { uni.navigateTo({ url: '/pages/sociAlize/Coffeegroup/Coffeeclass/CoffeeclassPage' + `?train=${trainStr}` }) return } // 工作日志 / 周报 / 流水账 // const map = { // '工作日志': { // type: 2, // title: '工作日志' // }, // '周报': { // type: 3, // title: '周报' // }, // '流水账': { // type: 4, // title: '流水账' // } // } // if (map[name]) { // const { // type, // title // } = map[name] // uni.navigateTo({ // url: `${base}/worklog/Logpage?from=organize&type=${type}&title=${encodeURIComponent(title)}&train=${trainStr}` // }) // return // } } // senderAvatarUrl let isGroupList = ref(false) //组列表 onLoad(option => { from.value = option?.from || '' if (option?.team) { try { team.value = JSON.parse(decodeURIComponent(option.team)) } catch { team.value = null } } // 进入页面就加载小组(如果展开) if (team.value?.castingTeamId && panel.subTeams) { fetchSubTeams(false) } if (option?.from === 'GroupList' || option?.Groupchat) { try { isGroupList.value = true team.value = JSON.parse(decodeURIComponent(option.Groupchat)) fetchSubTeams(false) } catch (e) { } } }) </script> <style lang="scss" scoped> @import '@/assets/styles/global.scss'; .content { min-height: 100vh; position: relative; background: #f2f2f2; } .overlay { position: absolute; inset: 0; background: #f2f2f2; } /* 固定header */ .fixed-header { position: fixed; left: 0; top: 0; width: 100%; z-index: 999; background: #f2f2f2; } .header-inner { position: relative; height: 100%; display: flex; align-items: center; justify-content: center; padding: 0 3%; } .left-box, .right-box { position: absolute; top: 0; bottom: 0; width: 120rpx; display: flex; align-items: center; } .left-box { left: 3%; justify-content: flex-start; } .right-box { right: 3%; justify-content: flex-end; } .center-box { flex: none; } .title-text { font-size: 32rpx; color: #111; font-weight: 600; } /* 滚动区域 */ .scroll-area { height: 100vh; box-sizing: border-box; } .wrap { width: 94%; margin: 0 auto; padding: 18rpx 0 0; } /* 卡片 */ .card { background: #ffffff; border-radius: 18rpx; padding: 18rpx; margin-bottom: 18rpx; } /* 顶部大组行 */ .group-row { display: flex; align-items: center; justify-content: space-between; } .group-left { display: flex; align-items: center; min-width: 0; } .group-title-wrap { margin-left: 16rpx; min-width: 0; } .group-title { font-size: 30rpx; color: #222; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 520rpx; } .time-text { font-size: 30rpx; color: #666; } /* ✅ 最后一条消息位置:紧贴标题下方,像图里“内容”那行 */ .notice-row { padding: 18rpx 0 14rpx; } .notice-text { font-size: 32rpx; color: #333; } /* 分割线:图里更厚一点 */ .divider { height: 2rpx; background: #d0d0d0; margin: 0; } /* 头像组合 */ .avatar-box { width: 96rpx; height: 96rpx; border-radius: 12rpx; overflow: hidden; background: #f2f2f2; } .img-fill { width: 100%; height: 100%; border-radius: 12rpx; } .avatar-one { width: 100%; height: 100%; } .avatar-two { width: 100%; height: 100%; display: flex; justify-content: space-between; } .img-two { width: 48rpx; height: 96rpx; border-radius: 12rpx; } .avatar-three { width: 100%; height: 100%; position: relative; } .img-three { position: absolute; width: 48rpx; height: 48rpx; border-radius: 12rpx; } .img-three.a { left: 0; top: 0; } .img-three.b { right: 0; top: 0; } .img-three.c { right: 0; bottom: 0; } .avatar-four { width: 100%; height: 100%; display: flex; flex-wrap: wrap; } .img-four { width: 48rpx; height: 48rpx; border-radius: 12rpx; } /* subcard */ .subcard { margin-top: 18rpx; background: #ececec; border-radius: 16rpx; padding: 16rpx; } .subcard-header, .section-header { display: flex; align-items: center; justify-content: space-between; padding: 6rpx 6rpx 12rpx; } .subcard-title { font-size: 34rpx; color: #333; font-weight: 600; } .section-title { font-size: 34rpx; color: #333; font-weight: 600; } .arrow-hit { width: 84rpx; height: 56rpx; display: flex; align-items: center; justify-content: flex-end; } .sub-loading, .sub-empty { padding: 14rpx 8rpx; font-size: 26rpx; color: #888; } /* 小组 item:按图——标题行 + 内容行 + 分割线 */ .subteam-row { display: flex; align-items: center; justify-content: space-between; padding: 16rpx 6rpx 14rpx; border-top: 2rpx solid #dcdcdc; } .subteam-left { display: flex; align-items: center; min-width: 0; } .subteam-avatar { width: 88rpx; height: 88rpx; border-radius: 14rpx; background: #eaeaea; margin-right: 16rpx; } .subteam-text { display: flex; flex-direction: column; min-width: 0; } .subteam-title { font-size: 34rpx; color: #333; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 520rpx; } .subteam-sub { margin-top: 14rpx; font-size: 32rpx; color: #333; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 520rpx; } /* grid 5列 */ .grid { display: flex; flex-wrap: wrap; padding: 4rpx 0 2rpx; } .grid-item { width: 20%; display: flex; flex-direction: column; align-items: center; padding: 10rpx 0 14rpx; } .icon-box { width: 72rpx; height: 72rpx; border-radius: 16rpx; background: #f2f2f2; display: flex; align-items: center; justify-content: center; } .icon-active { background: #f0a23a; } .grid-text { margin-top: 10rpx; font-size: 24rpx; color: #333; } /* footer */ .list-footer { padding: 18rpx 0 6rpx; text-align: center; } .footer-text { font-size: 24rpx; color: #999; } </style>然后点击这个<!-- 大组行:头像 + 组名(人数) + 时间 -->
text
<view @click="largegroupchat()" style="" class="group-row"> <view class="group-left"> <view class="avatar-box"> <!-- 1 --> <view v-if="team?.avatarFileArray && team.avatarFileArray.length === 1" class="avatar-one"> <image class="img-fill" :src="fullImg(team.avatarFileArray[0])" mode="aspectFill" /> </view> <!-- 2 --> <view v-else-if="team?.avatarFileArray && team.avatarFileArray.length === 2" class="avatar-two"> <image v-for="(img, i) in team.avatarFileArray" :key="i" class="img-two" :src="fullImg(img)" mode="aspectFill" /> </view> <!-- 3 --> <view v-else-if="team?.avatarFileArray && team.avatarFileArray.length === 3" class="avatar-three"> <image v-if="team.avatarFileArray[0]" :src="fullImg(team.avatarFileArray[0])" class="img-three a" mode="aspectFill" /> <image v-if="team.avatarFileArray[1]" :src="fullImg(team.avatarFileArray[1])" class="img-three b" mode="aspectFill" /> <image v-if="team.avatarFileArray[2]" :src="fullImg(team.avatarFileArray[2])" class="img-three c" mode="aspectFill" /> </view> <!-- 4 --> <view v-else-if="team?.avatarFileArray && team.avatarFileArray.length >= 4" class="avatar-four"> <image v-for="(img, i) in team.avatarFileArray.slice(0, 4)" :key="i" class="img-four" :src="fullImg(img)" mode="aspectFill" /> </view> <!-- fallback --> <view v-else class="avatar-one"> <image class="img-fill" :src="fullImg(team?.img || '')" mode="aspectFill" /> </view> <!-- 列表来的 --> <view v-if="isGroupList" class="avatar-one"> <image class="img-fill" :src="fullImg(team?.senderAvatarUrl || '')" mode="aspectFill" /> </view> </view> <!-- ✅ 组名(人数)严格按图 --> <view class="group-title-wrap"> <text class="group-title">{{ teamTitleShow }}</text> <view class="notice-row"> <text class="notice-text">{{ teamLastChatShow }}</text> </view> </view> </view> <view class="group-right"> <text class="time-text">{{ nowHHMM }}</text> </view> </view> <view class="divider" /> <!-- 组中组(小组) --> <view class="subcard"> <view class="subcard-header"> <text class="subcard-title">组中组</text> <!-- ✅ 只点右侧箭头折叠(避免整行点击误触) --> <view class="arrow-hit" @click.stop="togglePanel('subTeams')"> <uni-icons :type="panel.subTeams ? 'top' : 'bottom'" color="#666666" size="18" /> </view> </view> <view v-show="panel.subTeams"> <view v-if="subLoading" class="sub-loading">加载中...</view> <view v-else-if="!subTeams.length" class="sub-empty">暂无小组</view> <!-- 标题(人数) + 时间;下方 lastChatContent --> <view style="" v-for="(item, idx) in subTeams" :key="item.castingTeamId || idx" class="subteam-row" @click="enterSubTeam(item)"> <view class="subteam-left"> <image class="subteam-avatar" :src="fullImg(item.img || (item.avatarFileArray && item.avatarFileArray[0]) || '')" mode="aspectFill" /> <view class="subteam-text"> <text class="subteam-title">{{ subTeamTitleShow(item) }}</text> <text class="subteam-sub">{{ subTeamLastChatShow(item) }}</text> </view> </view> <view class="subteam-right"> <text class="time-text">{{ nowHHMM }}</text> </view> </view> <!-- 小组加载状态 --> <view v-if="subTeams.length" class="list-footer"> <text v-if="subLoadingMore" class="footer-text">加载中...</text> <text v-else-if="subFinished" class="footer-text">已经到底啦~</text> </view> </view> </view>大组或者小组进入前先调用这个api12.castingTeamAdminSettingShow 管理员设置显示接口 入口参数 Sting castingTeamId, 组id 返回值 joinLock 进群方式 teamMute 全体禁言 lock 锁定该群 makeUpClockInCount 补卡次数 forgetClockInTime 忘记补卡补卡天数

gesturePasswordId手势密码ID
stringPasswordId文字密码id
defaultSecretPasswordType默认密码类型
title 群昵称
notice群公告
在根据defaultSecretPasswordType 值 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')
})
}1是手势密码 2是文字密码 根据这个值调到不同的页面上 这个是gesture页面的内容 <!-- /pages/sociAlize/Coffee/setpassword/gesture.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" />
</view>
<view class="center-box">
<text class="title-text">{{ titleText }}</text>
</view>
<view class="right-box">
<text class="desc-text"></text>
</view>
</view>
</view>

text
<!-- 内容 --> <view class="page-body"> <view class="card"> <view class="row"> <text class="label">手势密码ID:</text> <text class="val">{{ state.secretGesturePasswordId || '未设置' }}</text> </view> <view class="row"> <text class="label">默认解锁方式:</text> <text class="val"> {{ state.defaultSecretPasswordType === GESTURE_TYPE ? '手势' : state.defaultSecretPasswordType === 2 ? '文字' : '未设置' }} </text> </view> <!-- ✅ 咖组模式提示 --> <view v-if="teamMode.enabled" class="row"> <text class="label">咖组:</text> <text class="val">{{ teamMode.castingTeamTitle || teamMode.castingTeamId }}</text> </view> </view> <!-- ✅ 只验证模式不显示 Tab(老逻辑) + ✅ 咖组设置模式只显示设置 --> <view class="tabs" v-if="!ui.checkOnly && !ui.onlySet"> <view class="tab" :class="{ active: ui.mode === 'set' }" @click="switchMode('set')">设置/重设</view> <view class="tab" :class="{ active: ui.mode === 'check' }" @click="switchMode('check')">验证 </view> </view> <view class="lock-wrap"> <GestureLock ref="lockRef" :tips="ui.tips" :min-points="4" :line-color="ui.line" :error-color="ui.error" :fill-color="ui.fill" :stroke-color="ui.stroke" @complete="onPatternComplete" /> </view> <!-- ✅ 只验证模式不显示按钮;咖组设置模式也不显示“设为默认” --> <view class="btns" v-if="!ui.checkOnly && !ui.onlySet"> <button class="btn primary" @click="setAsDefault" :disabled="!state.secretGesturePasswordId"> 设为默认(手势) </button> </view> </view> </view> </view> </view>
</template> <script setup> import { ref, reactive, computed } from 'vue' import { onLoad } from '@dcloudio/uni-app' import { storeToRefs } from 'pinia' import { useMainStore } from '@/store/index.js' import { getStatusBarHeight, getTitleBarHeight } from '@/utils/system.js' import ajax from '@/api/ajax.js' import GestureLock from '@/components/GestureLock/GestureLock.vue' const GESTURE_TYPE = 1 const store = useMainStore?.() || {} const { theme = ref('') } = storeToRefs?.(store) || { theme: ref('') } const statusBarHeight = ref(getStatusBarHeight()) const headerBarHeight = ref(getTitleBarHeight()) const ui = reactive({ mode: 'set', // set | check tips: '请绘制手势密码', line: '#007aff', error: '#fa3534', fill: '#ffffff', stroke: '#c0c0c0', checkOnly: false, // ✅ 新增:咖组设置进入密码,只允许 set(不显示 tab,不走 check) onlySet: false }) const state = reactive({ consumerId: '', secretGesturePasswordId: '', secretStringPasswordId: '', defaultSecretPasswordType: 0, redirectAfterCheck: '', redirectTarget: '', // ✅ 联系人跳转参数 FriendID: '', nickname: '', avatarUrl: '', targetObj: null, // ✅ 群聊跳转参数 GroupID: '', groupTitle: '', groupAvatarUrl: '', groupMeta: null, autoBack: false, lockPasswordIdFromRoute: false }) // ✅ 新增:咖组设置模式(castingTeamAdminSetting) const teamMode = reactive({ enabled: false, // teamSet=1 castingTeamId: '', castingTeamTitle: '', canSet: true, // Settings 已判断,这里再兜底 adminCache: { joinLock: undefined, mute: undefined, lock: undefined, makeUpClockInCount: undefined, forgetClockInTime: undefined } }) const titleText = computed(() => { if (teamMode.enabled) return '咖组进入密码' return '咖密' }) const lockRef = ref(null) const setFlow = reactive({ first: '', pendingConfirm: false, submitting: false }) // ✅ 你原来的群二次加密参数(保留) const Groupgesturetwice = ref({}) const isGroupMode = computed(() => !!Groupgesturetwice.value?.GroupID) // 群二次加密模式(chatGroupSetting) /** ========== 工具:兼容后端返回 ========== */ function respData(res) { return res?.data || res || {} } function isSuccess(res) { const d = respData(res) const r = d?.result if (r === 'success') return true if (r === true) return true if (r === 0 || r === '0') return true if (d?.code === 0) return true if (d?.success === true) return true return false } function respMsg(res) { const d = respData(res) return d?.remark || d?.msg || d?.message || d?.error || '' } // API(保留老的) const API = { passwordSave: (payload) => ajax.post({ url: 'password', method: 'post', data: payload }), validatePassword: (id, password) => ajax.post({ url: 'validatePassword', method: 'post', data: { id, password } }), consumerSecretPassword: ({ consumerId, passwordId, defaultSecretPasswordType }) => ajax.post({ url: 'consumerSecretPassword', method: 'post', data: { consumerId, passwordId, defaultSecretPasswordType } }), consumerSecretPasswordShow: (consumerId) => ajax.post({ url: 'consumerSecretPasswordShow', method: 'post', data: { consumerId } }), // ✅ 新增:咖组管理员设置 castingTeamAdminSettingShow: (castingTeamId) => ajax.post({ url: 'castingTeamAdminSettingShow', method: 'post', data: { castingTeamId } }), castingTeamAdminSetting: (payload) => ajax.post({ url: 'castingTeamAdminSetting', method: 'post', data: payload }) } function navigateBack() { uni.navigateBack() } function safeDecode(val) { if (!val) return '' try { return decodeURIComponent(val) } catch (e) { return val } } function safeParseJSON(val) { if (!val) return null try { return JSON.parse(val) } catch (e) { return null } } function buildUrlWithQuery(url, extra = {}) { if (!url) return '' const parts = url.split('?') const path = parts[0] const rawQuery = parts[1] || '' const params = {} if (rawQuery) { rawQuery.split('&').forEach((kv) => { if (!kv) return const i = kv.indexOf('=') if (i === -1) params[decodeURIComponent(kv)] = '' else { const k = kv.slice(0, i) const v = kv.slice(i + 1) params[decodeURIComponent(k)] = decodeURIComponent(v || '') } }) } Object.keys(extra).forEach((k) => { const v = extra[k] if (v === undefined || v === null || v === '') return params[k] = typeof v === 'object' ? JSON.stringify(v) : String(v) }) const qs = Object.keys(params) .map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`) .join('&') return qs ? `${path}?${qs}` : path } function goTo(url) { if (!url) return uni.redirectTo({ url, fail: () => { uni.navigateTo({ url, fail: () => { uni.reLaunch({ url, fail: () => { uni.switchTab({ url }) } }) } }) } }) } // ✅ 咖组设置:拉 show 缓存,避免覆盖其他管理员设置 async function refreshTeamAdminCache() { if (!teamMode.castingTeamId) return const res = await API.castingTeamAdminSettingShow(teamMode.castingTeamId).catch(() => ({})) const ds = respData(res)?.data || respData(res) || {} teamMode.adminCache.joinLock = ds?.joinLock ?? teamMode.adminCache.joinLock teamMode.adminCache.mute = ds?.teamMute ?? ds?.mute ?? teamMode.adminCache.mute teamMode.adminCache.lock = ds?.lock ?? teamMode.adminCache.lock teamMode.adminCache.makeUpClockInCount = ds?.makeUpClockInCount ?? teamMode.adminCache.makeUpClockInCount teamMode.adminCache.forgetClockInTime = ds?.forgetClockInTime ?? teamMode.adminCache.forgetClockInTime } // ✅ 咖组设置:写入密码到 castingTeamAdminSetting async function setTeamPassword({ gesturePasswordId, stringPasswordId, defaultSecretPasswordType }) { if (!teamMode.castingTeamId || !state.consumerId) throw new Error('缺少 castingTeamId/consumerId') // 先刷新 show 缓存 await refreshTeamAdminCache() const payload = { castingTeamId: teamMode.castingTeamId, consumerId: state.consumerId, gesturePasswordId: String(gesturePasswordId || ''), stringPasswordId: String(stringPasswordId || ''), defaultSecretPasswordType: Number(defaultSecretPasswordType || 0) } // 带回其他字段(避免覆盖) if (teamMode.adminCache.joinLock !== undefined) payload.joinLock = teamMode.adminCache.joinLock if (teamMode.adminCache.mute !== undefined) payload.mute = teamMode.adminCache.mute if (teamMode.adminCache.lock !== undefined) payload.lock = teamMode.adminCache.lock if (teamMode.adminCache.makeUpClockInCount !== undefined) payload.makeUpClockInCount = teamMode.adminCache .makeUpClockInCount if (teamMode.adminCache.forgetClockInTime !== undefined) payload.forgetClockInTime = teamMode.adminCache .forgetClockInTime const res = await API.castingTeamAdminSetting(payload).catch(() => ({})) if (!isSuccess(res)) { throw new Error(respMsg(res) || '设置失败') } } onLoad(async (option = {}) => { // consumerId:外部传入优先 if (option.consumerId) state.consumerId = String(option.consumerId) else if (store?.user?.id) state.consumerId = store.user.id else state.consumerId = uni.getStorageSync('consumerId') || '' // autoBack:设置成功自动返回 state.autoBack = String(option.autoBack || '') === '1' // decrypt 有传就认为是“只验证模式”(老逻辑) if (option.decrypt !== undefined) { ui.checkOnly = true ui.mode = 'check' ui.tips = '请绘制已设置的手势进行验证' state.redirectAfterCheck = '/pages/sociAlize/Coffee/Coffeeability' } // 显式指定模式(老逻辑) if (option.mode === 'set') ui.mode = 'set' if (option.mode === 'check') ui.mode = 'check' // ✅ 二次密码:路由传 passwordId 就直接用它,并锁住不被覆盖(老逻辑 + 咖组也复用) if (option.passwordId) { state.secretGesturePasswordId = String(option.passwordId || '') state.lockPasswordIdFromRoute = true } // redirect(老逻辑) if (option.redirect) state.redirectAfterCheck = safeDecode(option.redirect) // ✅ 联系人参数(老逻辑) if (option.FriendID) state.FriendID = String(option.FriendID || '') if (option.nickname) state.nickname = safeDecode(option.nickname) if (option.avatarUrl) state.avatarUrl = safeDecode(option.avatarUrl) // ✅ 群聊参数(老逻辑) if (option.GroupID) state.GroupID = String(option.GroupID || '') if (option.castingChatGroupId && !state.GroupID) state.GroupID = String(option.castingChatGroupId || '') if (option.groupTitle) state.groupTitle = safeDecode(option.groupTitle) if (option.groupAvatarUrl) state.groupAvatarUrl = safeDecode(option.groupAvatarUrl) if (option.groupMeta) { const gm = safeDecode(option.groupMeta) state.groupMeta = safeParseJSON(gm) || null } // ✅ target(老逻辑) if (option.target) { const decoded = safeDecode(option.target) const maybeObj = safeParseJSON(decoded) if (maybeObj) state.targetObj = maybeObj else state.redirectTarget = decoded } // ✅ 群设置二次加密参数(老逻辑保留) if (option?.Groupsecondary) { try { Groupgesturetwice.value = JSON.parse(decodeURIComponent(option?.Groupsecondary)) } catch (e) { Groupgesturetwice.value = {} } } // ✅ 新增:咖组设置进入密码(Settings.vue 跳转过来会带 teamSet=1 & castingTeamId) if (String(option?.teamSet || '') === '1' && option?.castingTeamId) { teamMode.enabled = true teamMode.castingTeamId = String(option.castingTeamId || '') teamMode.castingTeamTitle = safeDecode(option.castingTeamTitle || '') // 咖组设置:只允许 set ui.onlySet = true ui.checkOnly = false ui.mode = 'set' ui.tips = '请绘制手势密码(用于咖组进入)' // 如果没有带 passwordId(组已有密码 id 未知),这里保持空,passwordSave 会创建新密码 // 如果带了 passwordId,允许重设同一个 passwordId } /** * ✅ 关键:三种模式互不影响 * A) 咖组设置(teamMode.enabled):不拉个人咖密,不走群二次加密绑定逻辑 * B) 群二次加密(Groupgesturetwice.GroupID):不拉个人咖密(你原修复保留) * C) 普通个人咖密:refreshProfile */ if (teamMode.enabled) { // 咖组设置:不做 refreshProfile state.defaultSecretPasswordType = GESTURE_TYPE await refreshTeamAdminCache() return } if (Groupgesturetwice.value?.GroupID) { if (!state.lockPasswordIdFromRoute) state.secretGesturePasswordId = '' state.secretStringPasswordId = '' state.defaultSecretPasswordType = 0 } else { refreshProfile() } }) // ✅ 你原来的群二次加密设置(老逻辑保留) async function Groupsecondaryencryption(id) { let consumerId = uni.getStorageSync('consumerId') if (!id) return if (!Groupgesturetwice.value?.GroupID) return let option = { consumerId, castingChatGroupId: Groupgesturetwice.value.GroupID, castingSecret: true, gesturePasswordId: id, stringPasswordId: '', defaultSecretPasswordType: Groupgesturetwice.value.type } try { ajax.post({ url: 'chatGroupSetting', data: option, method: 'post' }).then(res => { if (res.data.result === 'success') { uni.showToast({ title: '添加成功', icon: 'success' }) setTimeout(() => uni.navigateBack(), 300) } }) } catch (e) { uni.showToast({ title: '设置失败', icon: 'none' }) } } async function refreshProfile() { try { if (!state.consumerId) return const res = await API.consumerSecretPasswordShow(state.consumerId) const data = res?.data || res if (!data) return if (!state.lockPasswordIdFromRoute) { state.secretGesturePasswordId = data.secretGesturePasswordId || state.secretGesturePasswordId || '' } state.secretStringPasswordId = data.secretStringPasswordId || '' state.defaultSecretPasswordType = typeof data.defaultSecretPasswordType === 'number' ? data.defaultSecretPasswordType : Number(data.defaultSecretPasswordType || 0) } catch (e) {} } function switchMode(m) { // ✅ 咖组设置 onlySet:不允许切到 check if (ui.onlySet && m !== 'set') return if (ui.checkOnly && m === 'set') return ui.mode = m ui.tips = m === 'set' ? (setFlow.pendingConfirm ? '请再次绘制确认' : (teamMode.enabled ? '请绘制手势密码(用于咖组进入)' : '请绘制手势密码')) : '请绘制已设置的手势进行验证' lockRef.value?.reset() } async function onPatternComplete(pwd) { if (ui.mode === 'set') await handleSetFlow(pwd) else await handleCheckFlow(pwd) } /* ==================== 设置流程 ==================== */ async function handleSetFlow(pwd) { if (!setFlow.pendingConfirm) { setFlow.first = pwd setFlow.pendingConfirm = true ui.tips = '请再次绘制确认' lockRef.value?.flashSuccess(300) return } if (pwd !== setFlow.first) { ui.tips = '与第一次不一致,请重试' setFlow.first = '' setFlow.pendingConfirm = false lockRef.value?.flashError() return } if (setFlow.submitting) return setFlow.submitting = true try { /** * ✅ 三种模式保存策略: * 1) 咖组设置(teamMode.enabled):默认创建新密码(id 不传),如果路由传了 passwordId 则允许重设该 id * 2) 群二次加密(isGroupMode):你原逻辑:必须新建(不带 id) * 3) 个人咖密:允许重设(带 state.secretGesturePasswordId) */ let saveId = undefined if (teamMode.enabled) { saveId = state.secretGesturePasswordId || undefined } else if (isGroupMode.value) { saveId = undefined } else { saveId = state.secretGesturePasswordId || undefined } const payload = { id: saveId, password: pwd, type: GESTURE_TYPE } const saveRes = await API.passwordSave(payload) const newId = saveRes?.data?.id || saveRes?.id if (!newId) throw new Error('保存失败') state.secretGesturePasswordId = newId // ✅ 咖组设置:写入 castingTeamAdminSetting if (teamMode.enabled) { await setTeamPassword({ gesturePasswordId: newId, stringPasswordId: '', defaultSecretPasswordType: GESTURE_TYPE }) ui.tips = '设置成功' lockRef.value?.flashSuccess() setFlow.first = '' setFlow.pendingConfirm = false uni.showToast({ title: '设置成功', icon: 'success' }) setTimeout(() => navigateBack(), 450) return } // ✅ 非咖组:保持你原逻辑 if (!isGroupMode.value && state.consumerId) { await API.consumerSecretPassword({ consumerId: state.consumerId, passwordId: newId, defaultSecretPasswordType: GESTURE_TYPE }) state.defaultSecretPasswordType = GESTURE_TYPE } else { state.defaultSecretPasswordType = GESTURE_TYPE } if (Groupgesturetwice.value?.GroupID) { await Groupsecondaryencryption(newId) } ui.tips = '设置成功' lockRef.value?.flashSuccess() setFlow.first = '' setFlow.pendingConfirm = false uni.showToast({ title: '设置成功', icon: 'success' }) if (state.autoBack) setTimeout(() => navigateBack(), 450) } catch (e) { ui.tips = '保存失败,请重试' lockRef.value?.flashError() } finally { setFlow.submitting = false } } /* ==================== 验证流程(解锁) ==================== */ async function handleCheckFlow(pwd) { // ✅ 咖组设置 onlySet:不走验证 if (teamMode.enabled) { ui.tips = '当前为设置模式' lockRef.value?.flashError() return } const id = state.secretGesturePasswordId if (!id) { ui.tips = '尚未设置手势密码' lockRef.value?.flashError() return } try { const res = await API.validatePassword(id, pwd) const result = res?.data?.result || res?.result if (result === 'success') { ui.tips = '验证成功' lockRef.value?.flashSuccess() uni.showToast({ title: '验证成功', icon: 'success' }) const base = state.redirectAfterCheck + '?decrypt=' || '/pages/sociAlize/Coffee/Coffeeability' const extra = {} // 联系人参数 if (state.FriendID) extra.FriendID = state.FriendID if (state.nickname) extra.nickname = state.nickname if (state.avatarUrl) extra.avatarUrl = state.avatarUrl if (state.targetObj) extra.target = state.targetObj // 群聊参数 if (state.GroupID) { extra.GroupID = state.GroupID extra.castingChatGroupId = state.GroupID } if (state.groupTitle) extra.groupTitle = state.groupTitle if (state.groupAvatarUrl) extra.groupAvatarUrl = state.groupAvatarUrl if (state.groupMeta) extra.groupMeta = state.groupMeta const base2 = base + (state.redirectTarget && (state.redirectTarget.startsWith('?') || state.redirectTarget.startsWith('&')) ? state.redirectTarget : '') const url = buildUrlWithQuery(base2, extra) goTo(url) } else { ui.tips = result || '密码错误' lockRef.value?.flashError() } } catch (e) { ui.tips = '验证失败,请稍后再试' lockRef.value?.flashError() } } async function setAsDefault() { // ✅ 咖组设置不支持设为个人默认 if (teamMode.enabled) { uni.showToast({ title: '咖组进入密码不支持设为个人默认', icon: 'none' }) return } if (isGroupMode.value) { uni.showToast({ title: '群二次加密不支持设为个人默认', icon: 'none' }) return } if (!state.secretGesturePasswordId || !state.consumerId) return try { await API.consumerSecretPassword({ consumerId: state.consumerId, passwordId: state.secretGesturePasswordId, defaultSecretPasswordType: GESTURE_TYPE }) state.defaultSecretPasswordType = GESTURE_TYPE uni.showToast({ title: '已设为默认(手势)', icon: 'success' }) } catch (e) { uni.showToast({ title: '设置默认失败', icon: 'none' }) } } </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; } .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; } .page-body { width: 94%; margin: 0 auto; padding-bottom: 40rpx; } .card { margin-top: 24rpx; padding: 24rpx; background: #fff; border-radius: 16rpx; .row { display: flex; margin-bottom: 12rpx; font-size: 28rpx; } .label { color: #666; width: 260rpx; } .val { color: #111; flex: 1; } } .tabs { margin-top: 24rpx; display: flex; background: #fff; border-radius: 16rpx; overflow: hidden; .tab { flex: 1; text-align: center; padding: 22rpx 0; font-size: 30rpx; color: #666; } .tab.active { color: #007aff; font-weight: 600; background: #f5f8ff; } } .lock-wrap { margin-top: 30rpx; background: #fff; border-radius: 16rpx; padding: 20rpx 0 40rpx; } .btns { margin-top: 24rpx; display: grid; grid-template-columns: 1fr 1fr; gap: 20rpx; .btn { border-radius: 12rpx; } .primary { background: #007aff; color: #fff; } } </style>这样到了这个页面就是解密了不是设置密码 然后这个是Textpassword文字 的页面<!-- /pages/sociAlize/Coffee/setpassword/Textpassword.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"> <u-icon name="arrow-left" size="26" color="#2C2C2C" /> </view>
text
<view class="center-box"> <text class="title-text">数字 + 字母密码解锁</text> </view> </view> </view> <!-- 可滚动内容区域 --> <scroll-view scroll-y class="scroll-container"> <view class="form-wrapper"> <view class="tip-text">密码(数字 + 字母 + 符号)</view> <u-form ref="formRef" :model="form" :rules="rules" labelPosition="top" labelWidth="210"> <u-form-item prop="password" label="密码"> <u-input v-model="form.password" :type="showPassword.password ? 'text' : 'password'" placeholder="请输入密码" maxlength="20" border="surround" clearable> <template #suffix> <u-icon :name="showPassword.password ? 'eye' : 'eye-off'" size="26" color="#7D7D7D" @click="togglePassword('password')" /> </template> </u-input> </u-form-item> <u-form-item prop="confirmPassword" label="再次输入密码"> <u-input v-model="form.confirmPassword" :type="showPassword.confirm ? 'text' : 'password'" placeholder="请再次输入密码" maxlength="20" border="surround" clearable> <template #suffix> <u-icon :name="showPassword.confirm ? 'eye' : 'eye-off'" size="26" color="#7D7D7D" @click="togglePassword('confirm')" /> </template> </u-input> </u-form-item> </u-form> <view class="resend-row"> <u-button type="info" plain shape="square" customStyle="width:180rpx;color:#C7801F;border-color:#C7801F;" @click="ConfirmPassword" :disabled="isCounting"> 确认 </u-button> </view> </view> </scroll-view> <u-toast ref="toastRef" /> </view> </view> </view>
</template> <script setup> import { ref, reactive, computed, onMounted, onBeforeUnmount } from 'vue' import { usecontacts } from '@/store/usecontacts.js' import { onLoad } from '@dcloudio/uni-app' import { useMainStore } from '@/store/index.js' import { storeToRefs } from 'pinia' import { getStatusBarHeight, getTitleBarHeight } from '@/utils/system.js' import ajax from '@/api/ajax.js' // contacts store(一次密码仍需要兼容) const storeer = usecontacts() const { pas } = storeToRefs(storeer) // 主题 const store = useMainStore() const { theme } = storeToRefs(store) // 顶部高度 const statusBarHeight = ref(getStatusBarHeight()) const headerBarHeight = ref(getTitleBarHeight()) const navigateBack = () => { uni.navigateBack() } // 表单 & 组件 ref const formRef = ref(null) const toastRef = ref(null) // 表单数据 const form = reactive({ id: '', password: '', confirmPassword: '', phone: '', smsCode: '', type: 2 // 1 手势 2 文字,本页是文字密码 }) // ✅ 承接二次密码/跳转参数(一次密码也兼容) // ✅ 同时兼容【联系人】与【群聊】 // - 联系人:FriendID / targetObj // - 群聊:GroupID / groupMeta const routeMeta = reactive({ isQuadratic: false, mode: 'set', // set | check checkOnly: false, passwordId: '', // ✅ validatePassword 用的 id(重点) redirect: '', // ✅ 验证成功跳转 // 联系人 FriendID: '', nickname: '', avatarUrl: '', targetObj: null, // 群聊 GroupID: '', groupTitle: '', groupAvatarUrl: '', groupMeta: null }) function safeDecode(val) { if (!val) return '' try { return decodeURIComponent(val) } catch (e) { return val } } function safeParseJSON(val) { if (!val) return null try { return JSON.parse(val) } catch (e) { return null } } function buildUrlWithQuery(url, extra = {}) { if (!url) return '' const parts = url.split('?') const path = parts[0] const rawQuery = parts[1] || '' const params = {} if (rawQuery) { rawQuery.split('&').forEach((kv) => { if (!kv) return const i = kv.indexOf('=') if (i === -1) params[decodeURIComponent(kv)] = '' else { const k = kv.slice(0, i) const v = kv.slice(i + 1) params[decodeURIComponent(k)] = decodeURIComponent(v || '') } }) } Object.keys(extra).forEach((k) => { const v = extra[k] if (v === undefined || v === null || v === '') return params[k] = String(v) }) const qs = Object.keys(params) .map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`) .join('&') return qs ? `${path}?${qs}` : path } function goTo(url) { if (!url) return uni.redirectTo({ url, fail: () => { uni.navigateTo({ url, fail: () => { uni.reLaunch({ url, fail: () => { uni.switchTab({ url }) } }) } }) } }) } function getRedirectUrl() { const base = routeMeta.redirect || '/pages/sociAlize/Coffee/Coffeeability' const extra = {} // ✅ 联系人参数 if (routeMeta.FriendID) extra.FriendID = routeMeta.FriendID if (routeMeta.nickname) extra.nickname = routeMeta.nickname if (routeMeta.avatarUrl) extra.avatarUrl = routeMeta.avatarUrl if (routeMeta.targetObj) extra.target = JSON.stringify(routeMeta.targetObj) // ✅ 群聊参数(关键) if (routeMeta.GroupID) { extra.GroupID = routeMeta.GroupID // 兼容你项目里可能用 castingChatGroupId extra.castingChatGroupId = routeMeta.GroupID } if (routeMeta.groupTitle) extra.groupTitle = routeMeta.groupTitle if (routeMeta.groupAvatarUrl) extra.groupAvatarUrl = routeMeta.groupAvatarUrl if (routeMeta.groupMeta) extra.groupMeta = JSON.stringify(routeMeta.groupMeta) return buildUrlWithQuery(base, extra) } // 显示密码开关 const showPassword = reactive({ password: false, confirm: false }) // 倒计时(保留你原逻辑) const countdown = ref(59) const isCounting = ref(false) let timer = null // 校验正则 const passwordReg = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[~!@#$%^&*()_+\-={}[\]|:";'<>?,./]).{8,20}$/ const phoneReg = /^1[3-9]\d{9}$/ // 表单校验规则 const rules = computed(() => ({ password: [{ required: true, message: '请输入密码', trigger: ['blur', 'change'] }, { validator: (rule, value) => passwordReg.test(value), message: '密码需 8-20 位,包含字母、数字及符号', trigger: ['blur', 'change'] } ], confirmPassword: [{ required: true, message: '请再次输入密码', trigger: ['blur', 'change'] }, { validator: (rule, value) => value === form.password, message: '两次输入的密码不一致', trigger: ['blur', 'change'] } ], phone: [{ required: true, message: '请输入手机号', trigger: ['blur', 'change'] }, { validator: (rule, value) => phoneReg.test(value), message: '请输入正确的手机号', trigger: ['blur', 'change'] } ], smsCode: [{ required: true, message: '请输入短信验证码', trigger: ['blur', 'change'] }, { validator: (rule, value) => /^\d{4,6}$/.test(value), message: '验证码格式不正确', trigger: ['blur', 'change'] } ] })) // Toast 封装 const showToast = (message, type = 'default') => { toastRef.value?.show({ message, type, position: 'bottom' }) } // ✅ 核心:验证二次/一次密码(validatePassword) async function ConfirmPassword() { if (!form.password) { showToast('请输入密码', 'warning') return } if (!form.confirmPassword) { showToast('请再次输入密码', 'warning') return } if (form.password !== form.confirmPassword) { showToast('两次输入的密码不一致', 'warning') return } // ✅ 重要:优先用 routeMeta.passwordId(二次密码 id) const validateId = routeMeta.passwordId || form.id || pas.value?.secretStringPasswordId || '' if (!validateId) { showToast('缺少密码ID', 'error') return } let option = { id: validateId, password: form.password } console.log('验证的密码参数', option) try { showToast('密码验证中...', 'default') const res = await ajax.post({ url: 'validatePassword', data: option }) console.log(res, '验证的消息') if (res?.data?.result === 'success') { const url = getRedirectUrl() // ✅ 这里用 goTo,确保跳到联系人 chat 或 群聊 groupChat goTo(url) } else { showToast(res?.data?.result || '验证失败', 'error') } } catch (err) { console.error('validatePassword error:', err) showToast(err?.msg || '验证失败', 'error') } } // 切换密码显示 const togglePassword = (type) => { if (type === 'password') showPassword.password = !showPassword.password else if (type === 'confirm') showPassword.confirm = !showPassword.confirm } // ✅ 页面参数初始化:兼容 quadratic(联系人/群聊) + decrypt(一次密码旧入口) onLoad((options = {}) => { // 1) 二次密码入口(Hiddenpage 传 quadratic) if (options.quadratic) { const decoded = safeDecode(options.quadratic) const obj = safeParseJSON(decoded) || {} routeMeta.isQuadratic = !!obj.ISquadratic routeMeta.mode = obj.mode || 'check' routeMeta.checkOnly = routeMeta.mode === 'check' routeMeta.passwordId = obj.passwordId || '' routeMeta.redirect = obj.redirect || '' // 联系人 routeMeta.FriendID = obj.FriendID || '' routeMeta.targetObj = obj.target || null routeMeta.nickname = obj.nickname || '' routeMeta.avatarUrl = obj.avatarUrl || '' // 群聊 routeMeta.GroupID = obj.GroupID || obj.castingChatGroupId || '' routeMeta.groupTitle = obj.groupTitle || obj.title || '' routeMeta.groupAvatarUrl = obj.groupAvatarUrl || obj.avatarUrl || '' routeMeta.groupMeta = obj.groupMeta || null return } // 2) 一次密码旧入口 if (options.decrypt !== undefined) { routeMeta.checkOnly = true routeMeta.mode = 'check' } if (options.mode) routeMeta.mode = options.mode if (options.passwordId) routeMeta.passwordId = String(options.passwordId || '') if (options.redirect) routeMeta.redirect = safeDecode(options.redirect) // 联系人参数兼容 if (options.FriendID) routeMeta.FriendID = String(options.FriendID || '') if (options.nickname) routeMeta.nickname = safeDecode(options.nickname) if (options.avatarUrl) routeMeta.avatarUrl = safeDecode(options.avatarUrl) if (options.target) { const t = safeDecode(options.target) routeMeta.targetObj = safeParseJSON(t) || null } // 群聊参数兼容 if (options.GroupID) routeMeta.GroupID = String(options.GroupID || '') if (options.castingChatGroupId && !routeMeta.GroupID) routeMeta.GroupID = String(options .castingChatGroupId || '') if (options.groupTitle) routeMeta.groupTitle = safeDecode(options.groupTitle) if (options.groupAvatarUrl) routeMeta.groupAvatarUrl = safeDecode(options.groupAvatarUrl) if (options.groupMeta) { const gm = safeDecode(options.groupMeta) routeMeta.groupMeta = safeParseJSON(gm) || null } // 3) 你原来的设置密码参数(保留) if (options.id) form.id = options.id || '' if (options.password) form.password = options.password if (options.phone) form.phone = options.phone form.type = Number(options.type || 2) }) onMounted(() => { statusBarHeight.value = getStatusBarHeight() headerBarHeight.value = getTitleBarHeight() }) onBeforeUnmount(() => { if (timer) { clearInterval(timer) timer = null } }) </script> <style lang="scss" scoped> @import '@/assets/styles/global.scss'; .app-container { background: #f2f2f2; min-height: 100vh; } .content { min-height: 100vh; position: relative; } .overlay { position: absolute; inset: 0; display: flex; flex-direction: column; } .header { width: 100%; } .header-inner { position: relative; display: flex; align-items: center; justify-content: center; height: 100%; padding: 0 32rpx; } .left-box { position: absolute; left: 32rpx; display: flex; align-items: center; height: 100%; } .center-box { display: flex; align-items: center; justify-content: center; } .title-text { font-size: 30rpx; font-weight: 600; color: #2c2c2c; } .scroll-container { flex: 1; } .form-wrapper { width: 94%; margin: 40rpx auto 80rpx; padding: 48rpx 36rpx 56rpx; background-color: #f5f5f5; border-radius: 24rpx; box-shadow: 0 12rpx 32rpx rgba(0, 0, 0, 0.04); } .tip-text { font-size: 24rpx; color: #828282; margin-bottom: 30rpx; } .resend-row { margin-top: 36rpx; display: flex; justify-content: flex-end; } </style>他们解密通过都跳到到这个页面 Groupchat uni.navigateTo({
url:'/pages/emitsgroupchat/createCrowd/groupChat'

}) 然后返回一个超级完整的CafeGroupDetails页面的内容 和包含所有内容的超级完整的gesture 页面和Textpassword 页面 打通所有的逻辑这些都是解密 之前的已经设置过密码了
之前的所有的页面的内容统统返回新功能 不能影响到之前所有的老功能的东西

Pensamento

Updating CafeGroupDetails with logic for password validation

The user wants updated code for three pages, introducing logic before entering large group or subTeam. This involves calling a function to determine the password type and navigating accordingly. I need to modify the CafeGroupDetails to check preconditions, check the response, and ensure old functionality isn't broken. I’ll also focus on properly parsing and utilizing the response fields.

Clarifying new flow for password check and groupChat navigation

I’m figuring out the logic for redirect after the password check. When the password type and ID match, users should go to the decrypt page; otherwise, they'll move straight to the groupChat page. I’ll also make sure we handle the old group entry properly when no password is set, preserving the previous flow.

Fixing logic for decrypt and group navigation

The team wants to always call the API before entry; if no password is required (defaultSecretPasswordType is 0/missing), it'll go directly. I need to update the gesture page to support team entry as well. It’s also crucial to fix the buggy URL building for a more accurate redirect, ensuring we don’t append unnecessary parameters.

Preserving backward compatibility and fixing URL handling

I need to adjust the URL building logic without breaking previous flows. If we're adding a decrypt param for team entry, the existing base URL logic leads to issues with appending empty query strings. I’ll improve it by checking if the URL already contains a decrypt parameter, then adjusting accordingly to preserve the old behavior.

Handling redirect logic and backwards compatibility

I’m adding robust handling for redirects, making sure that we only append the decrypt param when necessary, especially for groupChat. There’s a delicate balance between maintaining current behavior and implementing the new flow. I’ll use an optional route parameter, appendDecrypt, and introduce navType to specify whether we should use navigateTo or redirectTo. This ensures backward compatibility without breaking old flows.

Integrating group chat access and API calls

I need to call the relevant settings API before entering the group chat, especially for larger groups or subteams. I'll show a loading indicator, check success, and write a helper to fetch settings based on castingTeamId. I’ll pass necessary parameters like castingTeamId and team details to the group chat, ensuring compatibility with the current group chat page behavior. To ensure smooth navigation, I’ll set the group’s details into storage before navigating.

Setting up redirect and group navigation

For the group chat page, I’ll pass both father and team parameters even if it's not specified—I think it’s safe. I'll craft the URL with castingTeamId and additional params. In CafeGroupDetails, I'll implement the logic to fetch settings and redirect accordingly, handling different password types. I'll also adjust navigation behavior for different pages (gesture or text password). Additionally, I'll ensure all requests pass the necessary decrypt flag and handle any errors gracefully.

Simplifying parameter passing and flow management

I’ll maintain the old parameter passing for contacts and group chats while simplifying the flow after validation. I’ll check if state.redirectAfterCheck has a redirect URL and then handle decrypt and other parameters as needed. If they exist, I’ll update them properly without overwriting. For the text password flow, I’ll ensure it handles the correct parameters and supports the two password entries while keeping the default behavior consistent. I’ll also update titles and support new flow handling.

Clarifying flow and API integration with full code output

I need to ensure the correct routeMeta handling with passwordId and decrypt options set up. I’ll modify asynchronous functions for group navigation. To meet the user's request for "超级完整," I’m going to output full .vue code, keeping old functionality but integrating all necessary changes. I'll use helper functions and ensure API integration, maintaining consistency throughout. Code will include crucial fixes while ensuring everything is properly functional.

Refining logic for team and password navigation

I need to ensure that the pre-enter function handles cases where no team ID is provided. I’m focused on setting the right behavior for redirecting users through gesture or text password pages, or straight to the group chat. I'll customize URL generation based on type and ensure consistency with states like appendDecrypt and navType. For text password pages, it’s similar logic: extra parameters will be handled, like the decrypt flag.

Thought for 1m 32s

Resposta

下面我把 3 个页面都给你“超级完整”地整合了:

  • CafeGroupDetails.vue:点击【大组】或【小组】进入前 先调 castingTeamAdminSettingShow,按 defaultSecretPasswordType 跳转到 手势解密页 / 文字解密页;解密成功后统一进入 /pages/emitsgroupchat/createCrowd/groupChat
  • gesture.vue:新增 **“解密模式”**支持(不影响你原来的:个人咖密、群二次加密、咖组设置 teamSet 等老逻辑)
  • Textpassword.vue:同样新增 **“解密模式”**支持(不影响旧逻辑)

新增能力点(但不破坏旧功能):

  • 解密跳转支持参数:redirect(解密成功后去哪里)
  • 支持 appendDecrypt=0(新功能不再强行给 redirect URL 拼 decrypt 参数,默认仍保持老逻辑 append)
  • 支持 navType=navigate(新功能用 navigateTo 进入 groupChat,不覆盖历史栈;默认仍保持老逻辑 goTo)

1) /pages/sociAlize/Coffeegroup/CafeGroupDetails.vue(超级完整 + 新增“进入前先校验解密”逻辑)

vue
<template> <view :class="['app-container', theme]"> <view class="content"> <view class="overlay"> <!-- 固定 Header:滑动不移动 --> <view class="fixed-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" /> </view> <view class="center-box"> <text class="title-text">{{ pageTitle }}</text> </view> <view @click="EnteGroupSettings" class="right-box"> <uni-icons type="more-filled" color="#000000" size="22" /> </view> </view> </view> <!-- 滚动区域:顶部预留 header 高度 --> <scroll-view scroll-y class="scroll-area" :style="{ paddingTop: headerOffset + 'px' }" :lower-threshold="80" @scrolltolower="handleScrollToLower" > <view class="wrap"> <!-- 顶部:大组卡片 + 组中组 --> <view class="card"> <!-- 大组行:头像 + 组名(人数) + 时间 --> <view @click="largegroupchat()" class="group-row"> <view class="group-left"> <view class="avatar-box"> <!-- 1 --> <view v-if="team?.avatarFileArray && team.avatarFileArray.length === 1" class="avatar-one"> <image class="img-fill" :src="fullImg(team.avatarFileArray[0])" mode="aspectFill" /> </view> <!-- 2 --> <view v-else-if="team?.avatarFileArray && team.avatarFileArray.length === 2" class="avatar-two"> <image v-for="(img, i) in team.avatarFileArray" :key="i" class="img-two" :src="fullImg(img)" mode="aspectFill" /> </view> <!-- 3 --> <view v-else-if="team?.avatarFileArray && team.avatarFileArray.length === 3" class="avatar-three"> <image v-if="team.avatarFileArray[0]" :src="fullImg(team.avatarFileArray[0])" class="img-three a" mode="aspectFill" /> <image v-if="team.avatarFileArray[1]" :src="fullImg(team.avatarFileArray[1])" class="img-three b" mode="aspectFill" /> <image v-if="team.avatarFileArray[2]" :src="fullImg(team.avatarFileArray[2])" class="img-three c" mode="aspectFill" /> </view> <!-- 4 --> <view v-else-if="team?.avatarFileArray && team.avatarFileArray.length >= 4" class="avatar-four"> <image v-for="(img, i) in team.avatarFileArray.slice(0, 4)" :key="i" class="img-four" :src="fullImg(img)" mode="aspectFill" /> </view> <!-- fallback --> <view v-else class="avatar-one"> <image class="img-fill" :src="fullImg(team?.img || '')" mode="aspectFill" /> </view> <!-- 列表来的 --> <view v-if="isGroupList" class="avatar-one"> <image class="img-fill" :src="fullImg(team?.senderAvatarUrl || '')" mode="aspectFill" /> </view> </view> <!-- 组名(人数) --> <view class="group-title-wrap"> <text class="group-title">{{ teamTitleShow }}</text> <view class="notice-row"> <text class="notice-text">{{ teamLastChatShow }}</text> </view> </view> </view> <view class="group-right"> <text class="time-text">{{ nowHHMM }}</text> </view> </view> <view class="divider" /> <!-- 组中组(小组) --> <view class="subcard"> <view class="subcard-header"> <text class="subcard-title">组中组</text> <!-- 只点右侧箭头折叠 --> <view class="arrow-hit" @click.stop="togglePanel('subTeams')"> <uni-icons :type="panel.subTeams ? 'top' : 'bottom'" color="#666666" size="18" /> </view> </view> <view v-show="panel.subTeams"> <view v-if="subLoading" class="sub-loading">加载中...</view> <view v-else-if="!subTeams.length" class="sub-empty">暂无小组</view> <!-- 小组列表 --> <view v-for="(item, idx) in subTeams" :key="item.castingTeamId || idx" class="subteam-row" @click="enterSubTeam(item)" > <view class="subteam-left"> <image class="subteam-avatar" :src="fullImg(item.img || (item.avatarFileArray && item.avatarFileArray[0]) || '')" mode="aspectFill" /> <view class="subteam-text"> <text class="subteam-title">{{ subTeamTitleShow(item) }}</text> <text class="subteam-sub">{{ subTeamLastChatShow(item) }}</text> </view> </view> <view class="subteam-right"> <text class="time-text">{{ nowHHMM }}</text> </view> </view> <!-- 小组加载状态 --> <view v-if="subTeams.length" class="list-footer"> <text v-if="subLoadingMore" class="footer-text">加载中...</text> <text v-else-if="subFinished" class="footer-text">已经到底啦~</text> </view> </view> </view> </view> <!-- 工作流 --> <view class="card"> <view class="section-header"> <text class="section-title">工作流</text> <view class="arrow-hit" @click.stop="togglePanel('workflow')"> <uni-icons :type="panel.workflow ? 'top' : 'bottom'" color="#666666" size="18" /> </view> </view> <view v-show="panel.workflow" class="grid"> <view v-for="(it, i) in workflowItems" :key="i" class="grid-item" @click.stop="onToolClick(it, i)"> <view :class="['icon-box', it.active ? 'icon-active' : '']"> <uni-icons :type="it.icon" :color="it.active ? '#ffffff' : '#333333'" size="26" /> </view> <text class="grid-text">{{ it.name }}</text> </view> </view> </view> <!-- 信息流 --> <view class="card"> <view class="section-header"> <text class="section-title">信息流</text> <view class="arrow-hit" @click.stop="togglePanel('info')"> <uni-icons :type="panel.info ? 'top' : 'bottom'" color="#666666" size="18" /> </view> </view> <view v-show="panel.info" class="grid"> <view v-for="(it, i) in infoItems" :key="i" class="grid-item" @click.stop="onToolClick(it, i)"> <view class="icon-box"> <uni-icons :type="it.icon" color="#333333" size="26" /> </view> <text class="grid-text">{{ it.name }}</text> </view> </view> </view> <!-- 应用 --> <view class="card"> <view class="section-header"> <text class="section-title">应用</text> <view class="arrow-hit" @click.stop="togglePanel('apps')"> <uni-icons :type="panel.apps ? 'top' : 'bottom'" color="#666666" size="18" /> </view> </view> <view v-show="panel.apps" class="grid"> <view v-for="(it, i) in appItems" :key="i" class="grid-item" @click.stop="onToolClick(it, i)"> <view class="icon-box"> <uni-icons :type="it.icon" color="#333333" size="26" /> </view> <text class="grid-text">{{ it.name }}</text> </view> </view> </view> <view style="height: 60rpx;" /> </view> </scroll-view> </view> </view> </view> </template> <script setup> import ajax from '@/api/ajax.js' import { getStatusBarHeight, getTitleBarHeight } from '@/utils/system.js' import { ref, computed, inject, reactive } from 'vue' import { storeToRefs } from 'pinia' import { useMainStore } from '@/store/index.js' import { onLoad } from '@dcloudio/uni-app' // theme const store = useMainStore() const { theme } = storeToRefs(store) // header heights const statusBarHeight = ref(getStatusBarHeight()) const headerBarHeight = ref(getTitleBarHeight()) const headerOffset = computed(() => Number(statusBarHeight.value || 0) + Number(headerBarHeight.value || 0)) // img prefix const injectedWebProjectUrl = inject('webProjectUrl', '') const webProjectUrl = computed(() => injectedWebProjectUrl || getApp().globalData?.webProjectUrl || '') function fullImg(path = '') { const p = String(path || '').trim() if (!p) return '' if (/^https?:\/\//i.test(p)) return p return (webProjectUrl.value || '') + p } // params const from = ref('') const team = ref(null) // computed title const pageTitle = computed(() => team.value?.castingTeamTitle || '咖组详情') // 人数 const teamTitleShow = computed(() => { const t = team.value?.castingTeamTitle || '咖组' const count = Number(team.value?.count || 0) return count ? `${t}(${count})` : t }) // 组设置 function EnteGroupSettings() { if (!team.value?.castingTeamId) return uni.navigateTo({ url: '/pages/sociAlize/Coffeegroup/Createcoffeegroup/GroupSettings?teamId=' + team.value.castingTeamId }) } // 最后一条消息 const teamLastChatShow = computed(() => { const t = team.value const txt = t?.lastChatContent || t?.castingTeamNotice || '内容' return String(txt || '内容') }) // time const nowHHMM = computed(() => { const d = new Date() const hh = String(d.getHours()).padStart(2, '0') const mm = String(d.getMinutes()).padStart(2, '0') return `${hh}:${mm}` }) // back const navigateBack = () => uni.navigateBack() // 面板开关 const panel = reactive({ subTeams: true, workflow: true, info: true, apps: true }) function togglePanel(key) { panel[key] = !panel[key] if (key === 'subTeams' && panel.subTeams && !subTeams.value.length) { fetchSubTeams(false) } } // ---------- subTeams ---------- const subTeams = ref([]) const subLoading = ref(false) const subLoadingMore = ref(false) const subFinished = ref(false) const pageSize = 20 const ketword = ref('') function respData(res) { return res?.data || res || {} } function isSuccess(res) { const d = respData(res) const r = d?.result if (r === 'success') return true if (r === true) return true if (r === 0 || r === '0') return true if (d?.code === 0) return true if (d?.success === true) return true return false } function respMsg(res) { const d = respData(res) return d?.remark || d?.msg || d?.message || d?.error || '' } function fetchSubTeams(isLoadMore = false) { if (!team.value?.castingTeamId) return if (subFinished.value && isLoadMore) return if (isLoadMore) { if (subLoadingMore.value) return subLoadingMore.value = true } else { subLoading.value = true subFinished.value = false } const option = { consumerId: uni.getStorageSync('consumerId'), ketword: ketword.value || '', rowStartIdx: isLoadMore ? subTeams.value.length : 0, rowCount: pageSize, parentId: String(team.value.castingTeamId) } ajax .post({ url: 'castingTeamList', data: option, method: 'post' }) .then(res => { const d = respData(res) if (!isSuccess(res)) { uni.showToast({ title: respMsg(res) || '获取小组失败', icon: 'none' }) return } const list = d?.castingTeamConsumerObject || d?.castingTeamConsumerObjectArray || d?.castingTeamConsumerArray || d?.castingTeamArray || d?.data?.castingTeamConsumerObject || d?.data?.castingTeamConsumerObjectArray || [] const arr = Array.isArray(list) ? list : [] subTeams.value = isLoadMore ? subTeams.value.concat(arr) : arr if (!arr.length || arr.length < pageSize) subFinished.value = true }) .catch(() => { uni.showToast({ title: '获取小组失败', icon: 'none' }) }) .finally(() => { subLoading.value = false subLoadingMore.value = false }) } function handleScrollToLower() { if (!panel.subTeams) return if (!subLoading.value && !subLoadingMore.value && !subFinished.value) { fetchSubTeams(true) } } // 小组标题 function subTeamTitleShow(item) { const t = item?.castingTeamTitle || '小组' const count = Number(item?.count || 0) return count ? `${t}(${count})` : t } // 小组最后消息 function subTeamLastChatShow(item) { const txt = item?.lastChatContent || item?.castingTeamNotice || '内容' return String(txt || '内容') } /** ============================ * ✅ 新增:进入群聊前先调用 castingTeamAdminSettingShow * - defaultSecretPasswordType: 1 手势 / 2 文字 * - gesturePasswordId / stringPasswordId:用于 validatePassword * - 解密成功统一进入:/pages/emitsgroupchat/createCrowd/groupChat * - 不影响老功能:如果接口无密码/未设置 => 直接进入 groupChat ============================ */ function normalizeAdminSetting(res) { const d = respData(res) const ds = d?.data || d || {} return { defaultSecretPasswordType: Number(ds?.defaultSecretPasswordType || 0), gesturePasswordId: String(ds?.gesturePasswordId || ds?.secretGesturePasswordId || ''), stringPasswordId: String(ds?.stringPasswordId || ds?.secretStringPasswordId || ''), title: ds?.title, notice: ds?.notice, joinLock: ds?.joinLock, teamMute: ds?.teamMute, lock: ds?.lock, makeUpClockInCount: ds?.makeUpClockInCount, forgetClockInTime: ds?.forgetClockInTime } } // 组chat目标URL(统一新页面) // ⚠️ 这里我带上 from + team/father/castingTeamId(即使 groupChat 不用也不会报错) // 如果你 groupChat 有固定入参名,你改这里即可。 function buildGroupChatUrl(type, obj) { const base = '/pages/emitsgroupchat/createCrowd/groupChat' const castingTeamId = encodeURIComponent(String(obj?.castingTeamId || '')) if (type === 'largegroup') { const father = encodeURIComponent(JSON.stringify(obj || {})) return `${base}?from=largegroup&castingTeamId=${castingTeamId}&father=${father}` } const teamStr = encodeURIComponent(JSON.stringify(obj || {})) return `${base}?from=subTeam&castingTeamId=${castingTeamId}&team=${teamStr}` } async function preEnterTeam(type, obj) { const castingTeamId = obj?.castingTeamId if (!castingTeamId) return uni.showLoading({ title: '加载中...', mask: true }) let res try { res = await ajax.post({ url: 'castingTeamAdminSettingShow', method: 'post', data: { castingTeamId: String(castingTeamId) } }) } catch (e) { uni.hideLoading() uni.showToast({ title: '获取管理员设置失败', icon: 'none' }) return } uni.hideLoading() if (!isSuccess(res)) { uni.showToast({ title: respMsg(res) || '获取管理员设置失败', icon: 'none' }) return } const setting = normalizeAdminSetting(res) const redirectUrl = buildGroupChatUrl(type, obj) // ✅ 未设置密码 => 直接进群 if (!setting.defaultSecretPasswordType) { uni.navigateTo({ url: redirectUrl }) return } const decrypt = true // ✅ 1 手势 if (setting.defaultSecretPasswordType === 1 && setting.gesturePasswordId) { uni.navigateTo({ url: '/pages/sociAlize/Coffee/setpassword/gesture' + '?decrypt=' + decrypt + '&mode=check' + '&appendDecrypt=0' + // ✅ 新功能:不拼 decrypt 到 redirect '&navType=navigate' + // ✅ 新功能:解密成功用 navigateTo '&passwordId=' + (setting.gesturePasswordId || '') + '&redirect=' + encodeURIComponent(redirectUrl) }) return } // ✅ 2 文字 if (setting.defaultSecretPasswordType === 2 && setting.stringPasswordId) { uni.navigateTo({ url: '/pages/sociAlize/Coffee/setpassword/Textpassword' + '?decrypt=' + decrypt + '&mode=check' + '&appendDecrypt=0' + '&navType=navigate' + '&passwordId=' + (setting.stringPasswordId || '') + '&redirect=' + encodeURIComponent(redirectUrl) }) return } // ✅ defaultSecretPasswordType 有值但 id 缺失 => 当作无密码,直接进入 uni.navigateTo({ url: redirectUrl }) } // 大组点击 function largegroupchat() { if (!team.value) return preEnterTeam('largegroup', team.value) } // 小组点击 function enterSubTeam(item) { if (!item) return preEnterTeam('subTeam', item) } // ---------- items ---------- const workflowItems = ref([ { name: '组讯', icon: 'list', active: false }, { name: '通告', icon: 'notification', active: false }, { name: '工作日志', icon: 'compose', active: false }, { name: '周报', icon: 'paperplane', active: false }, { name: '流水账', icon: 'wallet', active: false } ]) const infoItems = ref([ { name: '视频会议', icon: 'videocam' }, { name: '直播', icon: 'videocam-filled' }, { name: '咖课', icon: 'mic' }, { name: '工作文档', icon: 'folder' }, { name: '围读文档', icon: 'paperclip' } ]) const appItems = ref([ { name: '日历', icon: 'calendar' }, { name: '天气', icon: 'cloud' }, { name: '咖信邮箱', icon: 'email' }, { name: '咖募', icon: 'personadd' }, { name: '咖友', icon: 'staff' }, { name: '咖安', icon: 'locked' }, { name: '地图', icon: 'map' }, { name: '打卡', icon: 'flag' }, { name: '相册', icon: 'image' } ]) function onToolClick(it, i) { if (!it) return const { name } = it const base = '/pages/sociAlize/Coffeegroup/workflow' const trainStr = encodeURIComponent(JSON.stringify(team.value)) if (name === '组讯') { uni.navigateTo({ url: `${base}/organizetrain/organizetrain?type=0&title=${encodeURIComponent('组训')}&train=${trainStr}` }) return } if (name === '通告') { uni.navigateTo({ url: `${base}/organizetrain/organizetrain?type=1&title=${encodeURIComponent('通告')}&train=${trainStr}` }) return } if (name === '工作日志') { uni.navigateTo({ url: '/pages/sociAlize/Coffeegroup/workflow/worklog/Logpage?from=organize' + '&type=2' + '&train=' + encodeURIComponent(JSON.stringify(team.value)) }) return } if (name === '周报') { uni.navigateTo({ url: '/pages/sociAlize/Coffeegroup/workflow/worklog/Logpage?from=organize' + '&type=3' + '&train=' + encodeURIComponent(JSON.stringify(team.value)) }) return } if (name === '流水账') { uni.navigateTo({ url: '/pages/sociAlize/Coffeegroup/workflow/worklog/Logpage?from=organize' + '&type=4' + '&train=' + encodeURIComponent(JSON.stringify(team.value)) }) return } if (name === '工作文档') { uni.navigateTo({ url: '/pages/sociAlize/Coffeegroup/information/workdocument' + `?type=6&title=${encodeURIComponent('工作文档')}` + `&train=${encodeURIComponent(JSON.stringify(team.value))}` }) return } if (name === '围读文档') { uni.navigateTo({ url: '/pages/sociAlize/Coffeegroup/information/workdocument' + `?type=7&title=${encodeURIComponent('围读文档')}` + `&train=${encodeURIComponent(JSON.stringify(team.value))}` }) return } if (name === '日历') { uni.navigateTo({ url: '/pages/sociAlize/Coffeegroup/application/calendar/calendar' }) return } if (name === '天气') { uni.navigateTo({ url: '/pages/sociAlize/Coffeegroup/application/weather/weather' }) return } if (name === '咖课') { uni.navigateTo({ url: '/pages/sociAlize/Coffeegroup/Coffeeclass/CoffeeclassPage' + `?train=${trainStr}` }) return } } // from list let isGroupList = ref(false) onLoad(option => { from.value = option?.from || '' if (option?.team) { try { team.value = JSON.parse(decodeURIComponent(option.team)) } catch { team.value = null } } // 进入页面就加载小组(如果展开) if (team.value?.castingTeamId && panel.subTeams) { fetchSubTeams(false) } if (option?.from === 'GroupList' || option?.Groupchat) { try { isGroupList.value = true team.value = JSON.parse(decodeURIComponent(option.Groupchat)) fetchSubTeams(false) } catch (e) {} } }) </script> <style lang="scss" scoped> @import '@/assets/styles/global.scss'; .content { min-height: 100vh; position: relative; background: #f2f2f2; } .overlay { position: absolute; inset: 0; background: #f2f2f2; } /* 固定header */ .fixed-header { position: fixed; left: 0; top: 0; width: 100%; z-index: 999; background: #f2f2f2; } .header-inner { position: relative; height: 100%; display: flex; align-items: center; justify-content: center; padding: 0 3%; } .left-box, .right-box { position: absolute; top: 0; bottom: 0; width: 120rpx; display: flex; align-items: center; } .left-box { left: 3%; justify-content: flex-start; } .right-box { right: 3%; justify-content: flex-end; } .center-box { flex: none; } .title-text { font-size: 32rpx; color: #111; font-weight: 600; } /* 滚动区域 */ .scroll-area { height: 100vh; box-sizing: border-box; } .wrap { width: 94%; margin: 0 auto; padding: 18rpx 0 0; } /* 卡片 */ .card { background: #ffffff; border-radius: 18rpx; padding: 18rpx; margin-bottom: 18rpx; } /* 顶部大组行 */ .group-row { display: flex; align-items: center; justify-content: space-between; } .group-left { display: flex; align-items: center; min-width: 0; } .group-title-wrap { margin-left: 16rpx; min-width: 0; } .group-title { font-size: 30rpx; color: #222; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 520rpx; } .time-text { font-size: 30rpx; color: #666; } /* 最后一条消息 */ .notice-row { padding: 18rpx 0 14rpx; } .notice-text { font-size: 32rpx; color: #333; } /* 分割线 */ .divider { height: 2rpx; background: #d0d0d0; margin: 0; } /* 头像组合 */ .avatar-box { width: 96rpx; height: 96rpx; border-radius: 12rpx; overflow: hidden; background: #f2f2f2; } .img-fill { width: 100%; height: 100%; border-radius: 12rpx; } .avatar-one { width: 100%; height: 100%; } .avatar-two { width: 100%; height: 100%; display: flex; justify-content: space-between; } .img-two { width: 48rpx; height: 96rpx; border-radius: 12rpx; } .avatar-three { width: 100%; height: 100%; position: relative; } .img-three { position: absolute; width: 48rpx; height: 48rpx; border-radius: 12rpx; } .img-three.a { left: 0; top: 0; } .img-three.b { right: 0; top: 0; } .img-three.c { right: 0; bottom: 0; } .avatar-four { width: 100%; height: 100%; display: flex; flex-wrap: wrap; } .img-four { width: 48rpx; height: 48rpx; border-radius: 12rpx; } /* subcard */ .subcard { margin-top: 18rpx; background: #ececec; border-radius: 16rpx; padding: 16rpx; } .subcard-header, .section-header { display: flex; align-items: center; justify-content: space-between; padding: 6rpx 6rpx 12rpx; } .subcard-title { font-size: 34rpx; color: #333; font-weight: 600; } .section-title { font-size: 34rpx; color: #333; font-weight: 600; } .arrow-hit { width: 84rpx; height: 56rpx; display: flex; align-items: center; justify-content: flex-end; } .sub-loading, .sub-empty { padding: 14rpx 8rpx; font-size: 26rpx; color: #888; } /* 小组 item */ .subteam-row { display: flex; align-items: center; justify-content: space-between; padding: 16rpx 6rpx 14rpx; border-top: 2rpx solid #dcdcdc; } .subteam-left { display: flex; align-items: center; min-width: 0; } .subteam-avatar { width: 88rpx; height: 88rpx; border-radius: 14rpx; background: #eaeaea; margin-right: 16rpx; } .subteam-text { display: flex; flex-direction: column; min-width: 0; } .subteam-title { font-size: 34rpx; color: #333; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 520rpx; } .subteam-sub { margin-top: 14rpx; font-size: 32rpx; color: #333; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 520rpx; } /* grid 5列 */ .grid { display: flex; flex-wrap: wrap; padding: 4rpx 0 2rpx; } .grid-item { width: 20%; display: flex; flex-direction: column; align-items: center; padding: 10rpx 0 14rpx; } .icon-box { width: 72rpx; height: 72rpx; border-radius: 16rpx; background: #f2f2f2; display: flex; align-items: center; justify-content: center; } .icon-active { background: #f0a23a; } .grid-text { margin-top: 10rpx; font-size: 24rpx; color: #333; } /* footer */ .list-footer { padding: 18rpx 0 6rpx; text-align: center; } .footer-text { font-size: 24rpx; color: #999; } </style>

2) /pages/sociAlize/Coffee/setpassword/gesture.vue(超级完整 + 新增“解密跳 groupChat”且不破坏老逻辑)

你原本的 gesture 页已经很强了,我只做了 “向后兼容”的增强

  • 新增 appendDecrypt(默认 true,老逻辑不变;新功能传 0)
  • 新增 navType(默认旧 goTo;新功能传 navigate)
  • 修复你原来 check 成功拼接 '?decrypt=' 的不稳定写法(现在统一走 buildUrlWithQuery)
vue
<!-- /pages/sociAlize/Coffee/setpassword/gesture.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" /> </view> <view class="center-box"> <text class="title-text">{{ titleText }}</text> </view> <view class="right-box"> <text class="desc-text"></text> </view> </view> </view> <!-- 内容 --> <view class="page-body"> <view class="card"> <view class="row"> <text class="label">手势密码ID:</text> <text class="val">{{ state.secretGesturePasswordId || '未设置' }}</text> </view> <view class="row"> <text class="label">默认解锁方式:</text> <text class="val"> {{ state.defaultSecretPasswordType === GESTURE_TYPE ? '手势' : state.defaultSecretPasswordType === 2 ? '文字' : '未设置' }} </text> </view> <!-- ✅ 咖组模式提示 --> <view v-if="teamMode.enabled" class="row"> <text class="label">咖组:</text> <text class="val">{{ teamMode.castingTeamTitle || teamMode.castingTeamId }}</text> </view> </view> <!-- ✅ 只验证模式不显示 Tab(老逻辑) + ✅ 咖组设置模式只显示设置 --> <view class="tabs" v-if="!ui.checkOnly && !ui.onlySet"> <view class="tab" :class="{ active: ui.mode === 'set' }" @click="switchMode('set')">设置/重设</view> <view class="tab" :class="{ active: ui.mode === 'check' }" @click="switchMode('check')">验证</view> </view> <view class="lock-wrap"> <GestureLock ref="lockRef" :tips="ui.tips" :min-points="4" :line-color="ui.line" :error-color="ui.error" :fill-color="ui.fill" :stroke-color="ui.stroke" @complete="onPatternComplete" /> </view> <!-- ✅ 只验证模式不显示按钮;咖组设置模式也不显示“设为默认” --> <view class="btns" v-if="!ui.checkOnly && !ui.onlySet"> <button class="btn primary" @click="setAsDefault" :disabled="!state.secretGesturePasswordId"> 设为默认(手势) </button> </view> </view> </view> </view> </view> </template> <script setup> import { ref, reactive, computed } from 'vue' import { onLoad } from '@dcloudio/uni-app' import { storeToRefs } from 'pinia' import { useMainStore } from '@/store/index.js' import { getStatusBarHeight, getTitleBarHeight } from '@/utils/system.js' import ajax from '@/api/ajax.js' import GestureLock from '@/components/GestureLock/GestureLock.vue' const GESTURE_TYPE = 1 const store = useMainStore?.() || {} const { theme = ref('') } = storeToRefs?.(store) || { theme: ref('') } const statusBarHeight = ref(getStatusBarHeight()) const headerBarHeight = ref(getTitleBarHeight()) const ui = reactive({ mode: 'set', // set | check tips: '请绘制手势密码', line: '#007aff', error: '#fa3534', fill: '#ffffff', stroke: '#c0c0c0', checkOnly: false, onlySet: false }) const state = reactive({ consumerId: '', secretGesturePasswordId: '', secretStringPasswordId: '', defaultSecretPasswordType: 0, redirectAfterCheck: '', redirectTarget: '', // ✅ 新增:跳转策略(不影响老逻辑) appendDecrypt: true, // 默认老逻辑保持:redirect 时带 decrypt 参数(存在即可) navType: 'auto', // auto/goTo,或 navigate // 联系人参数 FriendID: '', nickname: '', avatarUrl: '', targetObj: null, // 群聊参数 GroupID: '', groupTitle: '', groupAvatarUrl: '', groupMeta: null, autoBack: false, lockPasswordIdFromRoute: false }) // ✅ 咖组设置模式(castingTeamAdminSetting) const teamMode = reactive({ enabled: false, castingTeamId: '', castingTeamTitle: '', canSet: true, adminCache: { joinLock: undefined, mute: undefined, lock: undefined, makeUpClockInCount: undefined, forgetClockInTime: undefined } }) const titleText = computed(() => { if (teamMode.enabled) return '咖组进入密码' return '咖密' }) const lockRef = ref(null) const setFlow = reactive({ first: '', pendingConfirm: false, submitting: false }) const Groupgesturetwice = ref({}) const isGroupMode = computed(() => !!Groupgesturetwice.value?.GroupID) // ---------- utils ---------- function respData(res) { return res?.data || res || {} } function isSuccess(res) { const d = respData(res) const r = d?.result if (r === 'success') return true if (r === true) return true if (r === 0 || r === '0') return true if (d?.code === 0) return true if (d?.success === true) return true return false } function respMsg(res) { const d = respData(res) return d?.remark || d?.msg || d?.message || d?.error || '' } const API = { passwordSave: (payload) => ajax.post({ url: 'password', method: 'post', data: payload }), validatePassword: (id, password) => ajax.post({ url: 'validatePassword', method: 'post', data: { id, password } }), consumerSecretPassword: ({ consumerId, passwordId, defaultSecretPasswordType }) => ajax.post({ url: 'consumerSecretPassword', method: 'post', data: { consumerId, passwordId, defaultSecretPasswordType } }), consumerSecretPasswordShow: (consumerId) => ajax.post({ url: 'consumerSecretPasswordShow', method: 'post', data: { consumerId } }), castingTeamAdminSettingShow: (castingTeamId) => ajax.post({ url: 'castingTeamAdminSettingShow', method: 'post', data: { castingTeamId } }), castingTeamAdminSetting: (payload) => ajax.post({ url: 'castingTeamAdminSetting', method: 'post', data: payload }) } function navigateBack() { uni.navigateBack() } function safeDecode(val) { if (!val) return '' try { return decodeURIComponent(val) } catch (e) { return val } } function safeParseJSON(val) { if (!val) return null try { return JSON.parse(val) } catch (e) { return null } } function buildUrlWithQuery(url, extra = {}) { if (!url) return '' const parts = url.split('?') const path = parts[0] const rawQuery = parts[1] || '' const params = {} if (rawQuery) { rawQuery.split('&').forEach((kv) => { if (!kv) return const i = kv.indexOf('=') if (i === -1) params[decodeURIComponent(kv)] = '' else { const k = kv.slice(0, i) const v = kv.slice(i + 1) params[decodeURIComponent(k)] = decodeURIComponent(v || '') } }) } Object.keys(extra).forEach((k) => { const v = extra[k] if (v === undefined || v === null || v === '') return params[k] = typeof v === 'object' ? JSON.stringify(v) : String(v) }) const qs = Object.keys(params) .map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`) .join('&') return qs ? `${path}?${qs}` : path } // 老 goTo(保留) function goTo(url) { if (!url) return uni.redirectTo({ url, fail: () => { uni.navigateTo({ url, fail: () => { uni.reLaunch({ url, fail: () => { uni.switchTab({ url }) } }) } }) } }) } // ✅ 新增:根据 navType 决定跳转方式(默认不影响旧逻辑) function goByType(url) { if (!url) return if (state.navType === 'navigate') { uni.navigateTo({ url }) return } goTo(url) } // ---------- 咖组设置相关(老逻辑保留) ---------- async function refreshTeamAdminCache() { if (!teamMode.castingTeamId) return const res = await API.castingTeamAdminSettingShow(teamMode.castingTeamId).catch(() => ({})) const ds = respData(res)?.data || respData(res) || {} teamMode.adminCache.joinLock = ds?.joinLock ?? teamMode.adminCache.joinLock teamMode.adminCache.mute = ds?.teamMute ?? ds?.mute ?? teamMode.adminCache.mute teamMode.adminCache.lock = ds?.lock ?? teamMode.adminCache.lock teamMode.adminCache.makeUpClockInCount = ds?.makeUpClockInCount ?? teamMode.adminCache.makeUpClockInCount teamMode.adminCache.forgetClockInTime = ds?.forgetClockInTime ?? teamMode.adminCache.forgetClockInTime } async function setTeamPassword({ gesturePasswordId, stringPasswordId, defaultSecretPasswordType }) { if (!teamMode.castingTeamId || !state.consumerId) throw new Error('缺少 castingTeamId/consumerId') await refreshTeamAdminCache() const payload = { castingTeamId: teamMode.castingTeamId, consumerId: state.consumerId, gesturePasswordId: String(gesturePasswordId || ''), stringPasswordId: String(stringPasswordId || ''), defaultSecretPasswordType: Number(defaultSecretPasswordType || 0) } if (teamMode.adminCache.joinLock !== undefined) payload.joinLock = teamMode.adminCache.joinLock if (teamMode.adminCache.mute !== undefined) payload.mute = teamMode.adminCache.mute if (teamMode.adminCache.lock !== undefined) payload.lock = teamMode.adminCache.lock if (teamMode.adminCache.makeUpClockInCount !== undefined) payload.makeUpClockInCount = teamMode.adminCache.makeUpClockInCount if (teamMode.adminCache.forgetClockInTime !== undefined) payload.forgetClockInTime = teamMode.adminCache.forgetClockInTime const res = await API.castingTeamAdminSetting(payload).catch(() => ({})) if (!isSuccess(res)) throw new Error(respMsg(res) || '设置失败') } onLoad(async (option = {}) => { // consumerId:外部传入优先 if (option.consumerId) state.consumerId = String(option.consumerId) else if (store?.user?.id) state.consumerId = store.user.id else state.consumerId = uni.getStorageSync('consumerId') || '' state.autoBack = String(option.autoBack || '') === '1' // ✅ 新增:解密跳转控制(默认不影响旧逻辑) if (option.appendDecrypt !== undefined) state.appendDecrypt = String(option.appendDecrypt) !== '0' if (option.navType) state.navType = String(option.navType) === 'navigate' ? 'navigate' : 'auto' // decrypt 有传就认为是“只验证模式” if (option.decrypt !== undefined) { ui.checkOnly = true ui.mode = 'check' ui.tips = '请绘制已设置的手势进行验证' // ✅ redirect 可由外部指定(新功能用 groupChat),没传则走老默认 state.redirectAfterCheck = option.redirect ? safeDecode(option.redirect) : '/pages/sociAlize/Coffee/Coffeeability' } // 显式指定模式(老逻辑) if (option.mode === 'set') ui.mode = 'set' if (option.mode === 'check') ui.mode = 'check' // 路由传 passwordId => 直接用它做验证(解密) if (option.passwordId) { state.secretGesturePasswordId = String(option.passwordId || '') state.lockPasswordIdFromRoute = true } if (option.redirect && !state.redirectAfterCheck) state.redirectAfterCheck = safeDecode(option.redirect) // 联系人参数 if (option.FriendID) state.FriendID = String(option.FriendID || '') if (option.nickname) state.nickname = safeDecode(option.nickname) if (option.avatarUrl) state.avatarUrl = safeDecode(option.avatarUrl) // 群聊参数 if (option.GroupID) state.GroupID = String(option.GroupID || '') if (option.castingChatGroupId && !state.GroupID) state.GroupID = String(option.castingChatGroupId || '') if (option.groupTitle) state.groupTitle = safeDecode(option.groupTitle) if (option.groupAvatarUrl) state.groupAvatarUrl = safeDecode(option.groupAvatarUrl) if (option.groupMeta) { const gm = safeDecode(option.groupMeta) state.groupMeta = safeParseJSON(gm) || null } if (option.target) { const decoded = safeDecode(option.target) const maybeObj = safeParseJSON(decoded) if (maybeObj) state.targetObj = maybeObj else state.redirectTarget = decoded } // 群设置二次加密参数(老逻辑) if (option?.Groupsecondary) { try { Groupgesturetwice.value = JSON.parse(decodeURIComponent(option?.Groupsecondary)) } catch (e) { Groupgesturetwice.value = {} } } // ✅ 咖组设置进入密码(老逻辑保留) if (String(option?.teamSet || '') === '1' && option?.castingTeamId) { teamMode.enabled = true teamMode.castingTeamId = String(option.castingTeamId || '') teamMode.castingTeamTitle = safeDecode(option.castingTeamTitle || '') ui.onlySet = true ui.checkOnly = false ui.mode = 'set' ui.tips = '请绘制手势密码(用于咖组进入)' state.defaultSecretPasswordType = GESTURE_TYPE await refreshTeamAdminCache() return } // ✅ 三种模式互不干扰(老逻辑保留) if (Groupgesturetwice.value?.GroupID) { if (!state.lockPasswordIdFromRoute) state.secretGesturePasswordId = '' state.secretStringPasswordId = '' state.defaultSecretPasswordType = 0 } else { refreshProfile() } }) async function Groupsecondaryencryption(id) { let consumerId = uni.getStorageSync('consumerId') if (!id) return if (!Groupgesturetwice.value?.GroupID) return let option = { consumerId, castingChatGroupId: Groupgesturetwice.value.GroupID, castingSecret: true, gesturePasswordId: id, stringPasswordId: '', defaultSecretPasswordType: Groupgesturetwice.value.type } try { ajax.post({ url: 'chatGroupSetting', data: option, method: 'post' }).then(res => { if (res.data.result === 'success') { uni.showToast({ title: '添加成功', icon: 'success' }) setTimeout(() => uni.navigateBack(), 300) } }) } catch (e) { uni.showToast({ title: '设置失败', icon: 'none' }) } } async function refreshProfile() { try { if (!state.consumerId) return const res = await API.consumerSecretPasswordShow(state.consumerId) const data = res?.data || res if (!data) return if (!state.lockPasswordIdFromRoute) { state.secretGesturePasswordId = data.secretGesturePasswordId || state.secretGesturePasswordId || '' } state.secretStringPasswordId = data.secretStringPasswordId || '' state.defaultSecretPasswordType = typeof data.defaultSecretPasswordType === 'number' ? data.defaultSecretPasswordType : Number(data.defaultSecretPasswordType || 0) } catch (e) {} } function switchMode(m) { if (ui.onlySet && m !== 'set') return if (ui.checkOnly && m === 'set') return ui.mode = m ui.tips = m === 'set' ? setFlow.pendingConfirm ? '请再次绘制确认' : teamMode.enabled ? '请绘制手势密码(用于咖组进入)' : '请绘制手势密码' : '请绘制已设置的手势进行验证' lockRef.value?.reset() } async function onPatternComplete(pwd) { if (ui.mode === 'set') await handleSetFlow(pwd) else await handleCheckFlow(pwd) } /* ==================== 设置流程 ==================== */ async function handleSetFlow(pwd) { if (!setFlow.pendingConfirm) { setFlow.first = pwd setFlow.pendingConfirm = true ui.tips = '请再次绘制确认' lockRef.value?.flashSuccess(300) return } if (pwd !== setFlow.first) { ui.tips = '与第一次不一致,请重试' setFlow.first = '' setFlow.pendingConfirm = false lockRef.value?.flashError() return } if (setFlow.submitting) return setFlow.submitting = true try { let saveId = undefined if (teamMode.enabled) { saveId = state.secretGesturePasswordId || undefined } else if (isGroupMode.value) { saveId = undefined } else { saveId = state.secretGesturePasswordId || undefined } const payload = { id: saveId, password: pwd, type: GESTURE_TYPE } const saveRes = await API.passwordSave(payload) const newId = saveRes?.data?.id || saveRes?.id if (!newId) throw new Error('保存失败') state.secretGesturePasswordId = newId // ✅ 咖组设置:写入 castingTeamAdminSetting if (teamMode.enabled) { await setTeamPassword({ gesturePasswordId: newId, stringPasswordId: '', defaultSecretPasswordType: GESTURE_TYPE }) ui.tips = '设置成功' lockRef.value?.flashSuccess() setFlow.first = '' setFlow.pendingConfirm = false uni.showToast({ title: '设置成功', icon: 'success' }) setTimeout(() => navigateBack(), 450) return } // 非咖组:保持你原逻辑 if (!isGroupMode.value && state.consumerId) { await API.consumerSecretPassword({ consumerId: state.consumerId, passwordId: newId, defaultSecretPasswordType: GESTURE_TYPE }) state.defaultSecretPasswordType = GESTURE_TYPE } else { state.defaultSecretPasswordType = GESTURE_TYPE } if (Groupgesturetwice.value?.GroupID) { await Groupsecondaryencryption(newId) } ui.tips = '设置成功' lockRef.value?.flashSuccess() setFlow.first = '' setFlow.pendingConfirm = false uni.showToast({ title: '设置成功', icon: 'success' }) if (state.autoBack) setTimeout(() => navigateBack(), 450) } catch (e) { ui.tips = '保存失败,请重试' lockRef.value?.flashError() } finally { setFlow.submitting = false } } /* ==================== 验证流程(解锁) ==================== */ async function handleCheckFlow(pwd) { // 咖组设置 onlySet:不走验证 if (teamMode.enabled) { ui.tips = '当前为设置模式' lockRef.value?.flashError() return } const id = state.secretGesturePasswordId if (!id) { ui.tips = '尚未设置手势密码' lockRef.value?.flashError() return } try { const res = await API.validatePassword(id, pwd) const result = res?.data?.result || res?.result if (result === 'success') { ui.tips = '验证成功' lockRef.value?.flashSuccess() uni.showToast({ title: '验证成功', icon: 'success' }) // ✅ redirect 基础 const base = state.redirectAfterCheck || '/pages/sociAlize/Coffee/Coffeeability' const extra = {} // ✅ 默认保持老逻辑:redirect 加 decrypt 参数;新功能传 appendDecrypt=0 则不加 if (state.appendDecrypt) extra.decrypt = '1' // 联系人参数 if (state.FriendID) extra.FriendID = state.FriendID if (state.nickname) extra.nickname = state.nickname if (state.avatarUrl) extra.avatarUrl = state.avatarUrl if (state.targetObj) extra.target = state.targetObj // 群聊参数 if (state.GroupID) { extra.GroupID = state.GroupID extra.castingChatGroupId = state.GroupID } if (state.groupTitle) extra.groupTitle = state.groupTitle if (state.groupAvatarUrl) extra.groupAvatarUrl = state.groupAvatarUrl if (state.groupMeta) extra.groupMeta = state.groupMeta // redirectTarget(老逻辑) const mergedBase = base + (state.redirectTarget && (state.redirectTarget.startsWith('?') || state.redirectTarget.startsWith('&')) ? state.redirectTarget : '') const url = buildUrlWithQuery(mergedBase, extra) goByType(url) } else { ui.tips = result || '密码错误' lockRef.value?.flashError() } } catch (e) { ui.tips = '验证失败,请稍后再试' lockRef.value?.flashError() } } async function setAsDefault() { if (teamMode.enabled) { uni.showToast({ title: '咖组进入密码不支持设为个人默认', icon: 'none' }) return } if (isGroupMode.value) { uni.showToast({ title: '群二次加密不支持设为个人默认', icon: 'none' }) return } if (!state.secretGesturePasswordId || !state.consumerId) return try { await API.consumerSecretPassword({ consumerId: state.consumerId, passwordId: state.secretGesturePasswordId, defaultSecretPasswordType: GESTURE_TYPE }) state.defaultSecretPasswordType = GESTURE_TYPE uni.showToast({ title: '已设为默认(手势)', icon: 'success' }) } catch (e) { uni.showToast({ title: '设置默认失败', icon: 'none' }) } } </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; } .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; } .page-body { width: 94%; margin: 0 auto; padding-bottom: 40rpx; } .card { margin-top: 24rpx; padding: 24rpx; background: #fff; border-radius: 16rpx; .row { display: flex; margin-bottom: 12rpx; font-size: 28rpx; } .label { color: #666; width: 260rpx; } .val { color: #111; flex: 1; } } .tabs { margin-top: 24rpx; display: flex; background: #fff; border-radius: 16rpx; overflow: hidden; .tab { flex: 1; text-align: center; padding: 22rpx 0; font-size: 30rpx; color: #666; } .tab.active { color: #007aff; font-weight: 600; background: #f5f8ff; } } .lock-wrap { margin-top: 30rpx; background: #fff; border-radius: 16rpx; padding: 20rpx 0 40rpx; } .btns { margin-top: 24rpx; display: grid; grid-template-columns: 1fr 1fr; gap: 20rpx; .btn { border-radius: 12rpx; } .primary { background: #007aff; color: #fff; } } </style>

3) /pages/sociAlize/Coffee/setpassword/Textpassword.vue(超级完整 + 新增“解密跳 groupChat”且不破坏老逻辑)

vue
<!-- /pages/sociAlize/Coffee/setpassword/Textpassword.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"> <u-icon name="arrow-left" size="26" color="#2C2C2C" /> </view> <view class="center-box"> <text class="title-text">数字 + 字母密码解锁</text> </view> </view> </view> <!-- 可滚动内容区域 --> <scroll-view scroll-y class="scroll-container"> <view class="form-wrapper"> <view class="tip-text">密码(数字 + 字母 + 符号)</view> <u-form ref="formRef" :model="form" :rules="rules" labelPosition="top" labelWidth="210"> <u-form-item prop="password" label="密码"> <u-input v-model="form.password" :type="showPassword.password ? 'text' : 'password'" placeholder="请输入密码" maxlength="20" border="surround" clearable > <template #suffix> <u-icon :name="showPassword.password ? 'eye' : 'eye-off'" size="26" color="#7D7D7D" @click="togglePassword('password')" /> </template> </u-input> </u-form-item> <u-form-item prop="confirmPassword" label="再次输入密码"> <u-input v-model="form.confirmPassword" :type="showPassword.confirm ? 'text' : 'password'" placeholder="请再次输入密码" maxlength="20" border="surround" clearable > <template #suffix> <u-icon :name="showPassword.confirm ? 'eye' : 'eye-off'" size="26" color="#7D7D7D" @click="togglePassword('confirm')" /> </template> </u-input> </u-form-item> </u-form> <view class="resend-row"> <u-button type="info" plain shape="square" customStyle="width:180rpx;color:#C7801F;border-color:#C7801F;" @click="ConfirmPassword" :disabled="isCounting" > 确认 </u-button> </view> </view> </scroll-view> <u-toast ref="toastRef" /> </view> </view> </view> </template> <script setup> import { ref, reactive, computed, onMounted, onBeforeUnmount } from 'vue' import { usecontacts } from '@/store/usecontacts.js' import { onLoad } from '@dcloudio/uni-app' import { useMainStore } from '@/store/index.js' import { storeToRefs } from 'pinia' import { getStatusBarHeight, getTitleBarHeight } from '@/utils/system.js' import ajax from '@/api/ajax.js' // contacts store(一次密码仍需要兼容) const storeer = usecontacts() const { pas } = storeToRefs(storeer) // 主题 const store = useMainStore() const { theme } = storeToRefs(store) // 顶部高度 const statusBarHeight = ref(getStatusBarHeight()) const headerBarHeight = ref(getTitleBarHeight()) const navigateBack = () => { uni.navigateBack() } const formRef = ref(null) const toastRef = ref(null) // 表单数据 const form = reactive({ id: '', password: '', confirmPassword: '', phone: '', smsCode: '', type: 2 }) // ✅ 承接二次密码/跳转参数(一次密码也兼容) const routeMeta = reactive({ isQuadratic: false, mode: 'set', checkOnly: false, passwordId: '', redirect: '', // ✅ 新增:跳转策略(不影响老逻辑) appendDecrypt: true, navType: 'auto', // auto/goTo 或 navigate // 联系人 FriendID: '', nickname: '', avatarUrl: '', targetObj: null, // 群聊 GroupID: '', groupTitle: '', groupAvatarUrl: '', groupMeta: null }) function safeDecode(val) { if (!val) return '' try { return decodeURIComponent(val) } catch (e) { return val } } function safeParseJSON(val) { if (!val) return null try { return JSON.parse(val) } catch (e) { return null } } function buildUrlWithQuery(url, extra = {}) { if (!url) return '' const parts = url.split('?') const path = parts[0] const rawQuery = parts[1] || '' const params = {} if (rawQuery) { rawQuery.split('&').forEach((kv) => { if (!kv) return const i = kv.indexOf('=') if (i === -1) params[decodeURIComponent(kv)] = '' else { const k = kv.slice(0, i) const v = kv.slice(i + 1) params[decodeURIComponent(k)] = decodeURIComponent(v || '') } }) } Object.keys(extra).forEach((k) => { const v = extra[k] if (v === undefined || v === null || v === '') return params[k] = String(v) }) const qs = Object.keys(params) .map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`) .join('&') return qs ? `${path}?${qs}` : path } // 老 goTo(保留) function goTo(url) { if (!url) return uni.redirectTo({ url, fail: () => { uni.navigateTo({ url, fail: () => { uni.reLaunch({ url, fail: () => { uni.switchTab({ url }) } }) } }) } }) } // ✅ 新增:按 navType 跳转(默认不影响老逻辑) function goByType(url) { if (!url) return if (routeMeta.navType === 'navigate') { uni.navigateTo({ url }) return } goTo(url) } function getRedirectUrl() { const base = routeMeta.redirect || '/pages/sociAlize/Coffee/Coffeeability' const extra = {} // ✅ 默认保持老逻辑:redirect 加 decrypt 参数;新功能 appendDecrypt=0 则不加 if (routeMeta.appendDecrypt) extra.decrypt = '1' // 联系人参数 if (routeMeta.FriendID) extra.FriendID = routeMeta.FriendID if (routeMeta.nickname) extra.nickname = routeMeta.nickname if (routeMeta.avatarUrl) extra.avatarUrl = routeMeta.avatarUrl if (routeMeta.targetObj) extra.target = JSON.stringify(routeMeta.targetObj) // 群聊参数 if (routeMeta.GroupID) { extra.GroupID = routeMeta.GroupID extra.castingChatGroupId = routeMeta.GroupID } if (routeMeta.groupTitle) extra.groupTitle = routeMeta.groupTitle if (routeMeta.groupAvatarUrl) extra.groupAvatarUrl = routeMeta.groupAvatarUrl if (routeMeta.groupMeta) extra.groupMeta = JSON.stringify(routeMeta.groupMeta) return buildUrlWithQuery(base, extra) } // 显示密码开关 const showPassword = reactive({ password: false, confirm: false }) // 倒计时(保留你原逻辑) const countdown = ref(59) const isCounting = ref(false) let timer = null // 校验正则 const passwordReg = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[~!@#$%^&*()_+\-={}[\]|:";'<>?,./]).{8,20}$/ const phoneReg = /^1[3-9]\d{9}$/ // 表单校验规则 const rules = computed(() => ({ password: [ { required: true, message: '请输入密码', trigger: ['blur', 'change'] }, { validator: (rule, value) => passwordReg.test(value), message: '密码需 8-20 位,包含字母、数字及符号', trigger: ['blur', 'change'] } ], confirmPassword: [ { required: true, message: '请再次输入密码', trigger: ['blur', 'change'] }, { validator: (rule, value) => value === form.password, message: '两次输入的密码不一致', trigger: ['blur', 'change'] } ], phone: [ { required: true, message: '请输入手机号', trigger: ['blur', 'change'] }, { validator: (rule, value) => phoneReg.test(value), message: '请输入正确的手机号', trigger: ['blur', 'change'] } ], smsCode: [ { required: true, message: '请输入短信验证码', trigger: ['blur', 'change'] }, { validator: (rule, value) => /^\d{4,6}$/.test(value), message: '验证码格式不正确', trigger: ['blur', 'change'] } ] })) const showToast = (message, type = 'default') => { toastRef.value?.show({ message, type, position: 'bottom' }) } // ✅ 核心:验证密码(validatePassword) async function ConfirmPassword() { if (!form.password) { showToast('请输入密码', 'warning') return } if (!form.confirmPassword) { showToast('请再次输入密码', 'warning') return } if (form.password !== form.confirmPassword) { showToast('两次输入的密码不一致', 'warning') return } // ✅ 优先用 routeMeta.passwordId(解密入口传入) const validateId = routeMeta.passwordId || form.id || pas.value?.secretStringPasswordId || '' if (!validateId) { showToast('缺少密码ID', 'error') return } let option = { id: validateId, password: form.password } try { showToast('密码验证中...', 'default') const res = await ajax.post({ url: 'validatePassword', data: option }) if (res?.data?.result === 'success') { const url = getRedirectUrl() goByType(url) } else { showToast(res?.data?.result || '验证失败', 'error') } } catch (err) { showToast(err?.msg || '验证失败', 'error') } } const togglePassword = (type) => { if (type === 'password') showPassword.password = !showPassword.password else if (type === 'confirm') showPassword.confirm = !showPassword.confirm } // ✅ 页面参数初始化 onLoad((options = {}) => { // ✅ 新增:解密跳转控制(默认不影响老逻辑) if (options.appendDecrypt !== undefined) routeMeta.appendDecrypt = String(options.appendDecrypt) !== '0' if (options.navType) routeMeta.navType = String(options.navType) === 'navigate' ? 'navigate' : 'auto' // 1) 二次密码入口(Hiddenpage 传 quadratic) if (options.quadratic) { const decoded = safeDecode(options.quadratic) const obj = safeParseJSON(decoded) || {} routeMeta.isQuadratic = !!obj.ISquadratic routeMeta.mode = obj.mode || 'check' routeMeta.checkOnly = routeMeta.mode === 'check' routeMeta.passwordId = obj.passwordId || '' routeMeta.redirect = obj.redirect || '' routeMeta.FriendID = obj.FriendID || '' routeMeta.targetObj = obj.target || null routeMeta.nickname = obj.nickname || '' routeMeta.avatarUrl = obj.avatarUrl || '' routeMeta.GroupID = obj.GroupID || obj.castingChatGroupId || '' routeMeta.groupTitle = obj.groupTitle || obj.title || '' routeMeta.groupAvatarUrl = obj.groupAvatarUrl || obj.avatarUrl || '' routeMeta.groupMeta = obj.groupMeta || null return } // 2) 一次密码旧入口 if (options.decrypt !== undefined) { routeMeta.checkOnly = true routeMeta.mode = 'check' } if (options.mode) routeMeta.mode = options.mode if (options.passwordId) routeMeta.passwordId = String(options.passwordId || '') if (options.redirect) routeMeta.redirect = safeDecode(options.redirect) // 联系人参数兼容 if (options.FriendID) routeMeta.FriendID = String(options.FriendID || '') if (options.nickname) routeMeta.nickname = safeDecode(options.nickname) if (options.avatarUrl) routeMeta.avatarUrl = safeDecode(options.avatarUrl) if (options.target) { const t = safeDecode(options.target) routeMeta.targetObj = safeParseJSON(t) || null } // 群聊参数兼容 if (options.GroupID) routeMeta.GroupID = String(options.GroupID || '') if (options.castingChatGroupId && !routeMeta.GroupID) routeMeta.GroupID = String(options.castingChatGroupId || '') if (options.groupTitle) routeMeta.groupTitle = safeDecode(options.groupTitle) if (options.groupAvatarUrl) routeMeta.groupAvatarUrl = safeDecode(options.groupAvatarUrl) if (options.groupMeta) { const gm = safeDecode(options.groupMeta) routeMeta.groupMeta = safeParseJSON(gm) || null } // 3) 你原来的设置密码参数(保留) if (options.id) form.id = options.id || '' if (options.password) form.password = options.password if (options.phone) form.phone = options.phone form.type = Number(options.type || 2) }) onMounted(() => { statusBarHeight.value = getStatusBarHeight() headerBarHeight.value = getTitleBarHeight() }) onBeforeUnmount(() => { if (timer) { clearInterval(timer) timer = null } }) </script> <style lang="scss" scoped> @import '@/assets/styles/global.scss'; .app-container { background: #f2f2f2; min-height: 100vh; } .content { min-height: 100vh; position: relative; } .overlay { position: absolute; inset: 0; display: flex; flex-direction: column; } .header { width: 100%; } .header-inner { position: relative; display: flex; align-items: center; justify-content: center; height: 100%; padding: 0 32rpx; } .left-box { position: absolute; left: 32rpx; display: flex; align-items: center; height: 100%; } .center-box { display: flex; align-items: center; justify-content: center; } .title-text { font-size: 30rpx; font-weight: 600; color: #2c2c2c; } .scroll-container { flex: 1; } .form-wrapper { width: 94%; margin: 40rpx auto 80rpx; padding: 48rpx 36rpx 56rpx; background-color: #f5f5f5; border-radius: 24rpx; box-shadow: 0 12rpx 32rpx rgba(0, 0, 0, 0.04); } .tip-text { font-size: 24rpx; color: #828282; margin-bottom: 30rpx; } .resend-row { margin-top: 36rpx; display: flex; justify-content: flex-end; } </style>

你现在的新流程是这样(不影响旧功能)

CafeGroupDetails 点击大组/小组
➡️ 先调 castingTeamAdminSettingShow(castingTeamId)

  • defaultSecretPasswordType==1 ➜ 跳 /setpassword/gesture?decrypt=1&mode=check&passwordId=gesturePasswordId&redirect=groupChatUrl
  • defaultSecretPasswordType==2 ➜ 跳 /setpassword/Textpassword?decrypt=1&mode=check&passwordId=stringPasswordId&redirect=groupChatUrl
  • 没有设置密码 ➜ 直接进 /pages/emitsgroupchat/createCrowd/groupChat?...

gesture / Textpassword 解密通过
➡️ 按 redirect 进入 groupChat(用 navigateTo,不覆盖历史栈)


如果你把 groupChat 的真实入参(比如必须要 castingTeamId 或必须要 groupInfo 的字段名)贴一下,我可以把 buildGroupChatUrl() 再精确到你那边的参数规则(现在我已经最大程度“兼容式”带上了 from + castingTeamId + team/father)。

Compartilhar esta Q&A