这个是CafeGroupDetails页面的内容<template> <view :class="...
बनाया गया: 8 जनवरी 2026
का उपयोग करके उत्तर दिया गया GPT-5.2 Thinking द्वारा Chat01
बनाया गया: 8 जनवरी 2026
का उपयोग करके उत्तर दिया गया GPT-5.2 Thinking द्वारा Chat01
这个是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>
</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 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>
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>
</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="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, 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({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>
url:'/pages/emitsgroupchat/createCrowd/groupChat'
}) 然后返回一个超级完整的CafeGroupDetails页面的内容 和包含所有内容的超级完整的gesture 页面和Textpassword 页面 打通所有的逻辑这些都是解密 之前的已经设置过密码了
之前的所有的页面的内容统统返回新功能 不能影响到之前所有的老功能的东西
下面我把 3 个页面都给你“超级完整”地整合了:
CafeGroupDetails.vue:点击【大组】或【小组】进入前 先调 castingTeamAdminSettingShow,按 defaultSecretPasswordType 跳转到 手势解密页 / 文字解密页;解密成功后统一进入 /pages/emitsgroupchat/createCrowd/groupChatgesture.vue:新增 **“解密模式”**支持(不影响你原来的:个人咖密、群二次加密、咖组设置 teamSet 等老逻辑)Textpassword.vue:同样新增 **“解密模式”**支持(不影响旧逻辑)新增能力点(但不破坏旧功能):
- 解密跳转支持参数:
redirect(解密成功后去哪里)- 支持
appendDecrypt=0(新功能不再强行给 redirect URL 拼decrypt参数,默认仍保持老逻辑 append)- 支持
navType=navigate(新功能用navigateTo进入 groupChat,不覆盖历史栈;默认仍保持老逻辑 goTo)
/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>
/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>
/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=groupChatUrldefaultSecretPasswordType==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)。