聊天修改

This commit is contained in:
18631081161 2026-01-21 19:36:36 +08:00
parent 3468be4716
commit b5bc4cd405
28 changed files with 1802 additions and 711 deletions

View File

@ -76,3 +76,17 @@ export function getSearchBanner() {
export function setSearchBanner(imageUrl: string) {
return request.post('/admin/config/searchBanner', { imageUrl })
}
/**
*
*/
export function getButlerQrcode() {
return request.get('/admin/config/butlerQrcode')
}
/**
*
*/
export function setButlerQrcode(imageUrl: string) {
return request.post('/admin/config/butlerQrcode', { imageUrl })
}

View File

@ -66,3 +66,13 @@ export function createTestUsers(count: number, gender?: number): Promise<number[
export function deleteUser(id: number): Promise<void> {
return request.delete(`/admin/users/${id}`)
}
/**
*
* @param id ID
* @param contactCount
* @returns
*/
export function updateContactCount(id: number, contactCount: number): Promise<void> {
return request.put(`/admin/users/${id}/contact-count`, { contactCount })
}

View File

@ -57,6 +57,29 @@
</div>
</el-form-item>
<!-- 管家二维码设置 -->
<el-form-item label="管家二维码">
<div class="qrcode-upload">
<el-upload
class="qrcode-uploader"
:action="uploadUrl"
:headers="uploadHeaders"
:show-file-list="false"
:on-success="handleQrcodeSuccess"
:before-upload="beforeAvatarUpload"
accept="image/*"
>
<img v-if="configForm.butlerQrcode" :src="getFullUrl(configForm.butlerQrcode)" class="qrcode-preview" />
<el-icon v-else class="qrcode-uploader-icon"><Plus /></el-icon>
</el-upload>
<div class="avatar-tip">
<p>建议尺寸300x300像素</p>
<p>支持格式JPGPNG</p>
<p>小程序"联系我们"页面展示的二维码</p>
</div>
</div>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="saveBasicConfig" :loading="saving">
保存配置
@ -119,7 +142,9 @@ import {
getPrivacyPolicy,
setPrivacyPolicy,
getSearchBanner,
setSearchBanner
setSearchBanner,
getButlerQrcode,
setButlerQrcode
} from '@/api/config'
import { useUserStore } from '@/stores/user'
@ -131,7 +156,8 @@ const activeTab = ref('basic')
const configForm = ref({
defaultAvatar: '',
searchBanner: ''
searchBanner: '',
butlerQrcode: ''
})
const agreementForm = ref({
@ -157,9 +183,10 @@ const getFullUrl = (url) => {
const loadConfig = async () => {
try {
const [avatarRes, bannerRes] = await Promise.all([
const [avatarRes, bannerRes, qrcodeRes] = await Promise.all([
getDefaultAvatar(),
getSearchBanner()
getSearchBanner(),
getButlerQrcode()
])
if (avatarRes) {
configForm.value.defaultAvatar = avatarRes.avatarUrl || ''
@ -167,6 +194,9 @@ const loadConfig = async () => {
if (bannerRes) {
configForm.value.searchBanner = bannerRes.imageUrl || ''
}
if (qrcodeRes) {
configForm.value.butlerQrcode = qrcodeRes.imageUrl || ''
}
} catch (error) {
console.error('加载配置失败:', error)
}
@ -209,6 +239,15 @@ const handleBannerSuccess = (response) => {
}
}
const handleQrcodeSuccess = (response) => {
if (response.code === 0 && response.data) {
configForm.value.butlerQrcode = response.data.url
ElMessage.success('上传成功')
} else {
ElMessage.error(response.message || '上传失败')
}
}
const beforeAvatarUpload = (file) => {
const isImage = file.type.startsWith('image/')
const isLt2M = file.size / 1024 / 1024 < 2
@ -234,6 +273,9 @@ const saveBasicConfig = async () => {
if (configForm.value.searchBanner) {
promises.push(setSearchBanner(configForm.value.searchBanner))
}
if (configForm.value.butlerQrcode) {
promises.push(setButlerQrcode(configForm.value.butlerQrcode))
}
if (promises.length > 0) {
await Promise.all(promises)
ElMessage.success('保存成功')
@ -401,6 +443,47 @@ onMounted(() => {
object-fit: cover;
}
.qrcode-upload {
display: flex;
align-items: flex-start;
gap: 20px;
}
.qrcode-uploader {
width: 150px;
height: 150px;
}
.qrcode-uploader :deep(.el-upload) {
border: 1px dashed var(--el-border-color);
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: var(--el-transition-duration-fast);
width: 150px;
height: 150px;
display: flex;
align-items: center;
justify-content: center;
}
.qrcode-uploader :deep(.el-upload:hover) {
border-color: var(--el-color-primary);
}
.qrcode-uploader-icon {
font-size: 28px;
color: #8c939d;
}
.qrcode-preview {
width: 150px;
height: 150px;
display: block;
object-fit: cover;
}
.agreement-editor {
padding: 20px 0;
}

View File

@ -8,7 +8,7 @@ import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ArrowLeft } from '@element-plus/icons-vue'
import StatusTag from '@/components/StatusTag/index.vue'
import { getUserDetail, updateUserStatus } from '@/api/user'
import { getUserDetail, updateUserStatus, updateContactCount } from '@/api/user'
import { getFullImageUrl } from '@/utils/image'
import type { UserDetail } from '@/types/user.d'
@ -78,6 +78,34 @@ const handleToggleStatus = async () => {
}
}
//
const handleEditContactCount = async () => {
if (!userDetail.value) return
try {
const { value } = await ElMessageBox.prompt(
'请输入新的联系次数',
'修改联系次数',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
inputValue: String(userDetail.value.contactCount),
inputPattern: /^\d+$/,
inputErrorMessage: '请输入有效的数字'
}
)
const newCount = parseInt(value, 10)
await updateContactCount(userId.value, newCount)
ElMessage.success('修改成功')
fetchUserDetail()
} catch (error) {
if (error !== 'cancel') {
console.error('修改联系次数失败:', error)
}
}
}
//
const formatTime = (time: string) => {
if (!time) return '-'
@ -280,7 +308,16 @@ onMounted(() => {
{{ formatTime(userDetail.memberExpireTime) }}
</el-descriptions-item>
<el-descriptions-item label="剩余联系次数">
{{ userDetail.contactCount }}
<span>{{ userDetail.contactCount }}</span>
<el-button
type="primary"
link
size="small"
style="margin-left: 8px;"
@click="handleEditContactCount"
>
修改
</el-button>
</el-descriptions-item>
<el-descriptions-item label="注册时间">
{{ formatTime(userDetail.createTime) }}

View File

@ -9,13 +9,16 @@ import * as fc from 'fast-check'
/**
* Message type constants
* 与后端 MessageType 枚举保持一致
*/
const MessageType = {
TEXT: 1,
VOICE: 2,
IMAGE: 3,
EXCHANGE_WECHAT: 4,
EXCHANGE_PHOTO: 5
EXCHANGE_WECHAT_RESULT: 5,
EXCHANGE_PHOTO: 6,
EXCHANGE_PHOTO_RESULT: 7
}
/**

View File

@ -16,9 +16,21 @@ export async function getSessions() {
const response = await get('/chat/sessions')
// 统一返回格式
if (response && response.code === 0) {
// 后端字段映射到前端字段
const list = Array.isArray(response.data) ? response.data : (response.data?.list || [])
const mappedList = list.map(item => ({
sessionId: item.sessionId,
targetUserId: item.otherUserId,
targetNickname: item.otherNickname,
targetAvatar: item.otherAvatar,
lastMessage: item.lastMessageContent,
lastMessageType: item.lastMessageType,
lastMessageTime: item.lastMessageTime,
unreadCount: item.unreadCount
}))
return {
success: true,
data: Array.isArray(response.data) ? response.data : (response.data?.list || [])
data: mappedList
}
}
return { success: false, data: [] }
@ -104,6 +116,17 @@ export async function getUnreadCount() {
return response
}
/**
* 获取或创建会话
*
* @param {number} targetUserId - 目标用户ID
* @returns {Promise<Object>} 会话ID
*/
export async function getOrCreateSession(targetUserId) {
const response = await get('/chat/session', { targetUserId })
return response
}
/**
* 上传语音文件
*
@ -153,5 +176,6 @@ export default {
exchangePhoto,
respondExchange,
getUnreadCount,
getOrCreateSession,
uploadVoice
}

View File

@ -137,10 +137,22 @@ export async function getMyUnlocked(pageIndex = 1, pageSize = 20) {
return response
}
/**
* 检查是否已解锁目标用户
*
* @param {number} targetUserId - 目标用户ID
* @returns {Promise<Object>} 解锁状态 { isUnlocked, remainingUnlockQuota }
*/
export async function checkUnlock(targetUserId) {
const response = await get('/interact/checkUnlock', { targetUserId })
return response
}
export default {
recordView,
favorite,
unlock,
checkUnlock,
report,
getViewedMe,
getMyViewed,

View File

@ -21,7 +21,7 @@ const ENV = {
}
// 当前环境 - 开发时使用 development打包时改为 production
const CURRENT_ENV = 'development'
const CURRENT_ENV = 'production'
// 导出配置
export const config = {

View File

@ -141,6 +141,13 @@
"navigationStyle": "custom",
"navigationBarTitleText": "协议"
}
},
{
"path": "pages/butler/index",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "联系我们"
}
}
],
"globalStyle": {

View File

@ -0,0 +1,304 @@
<template>
<view class="butler-page">
<!-- 页面加载状态 -->
<Loading type="page" :loading="pageLoading" />
<!-- 顶部背景图 -->
<view class="top-bg">
<image src="/static/title_bg.png" mode="aspectFill" class="bg-img" />
</view>
<!-- 自定义导航栏 -->
<view class="custom-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="navbar-content">
<view class="navbar-back" @click="handleBack">
<text class="back-icon"></text>
</view>
<text class="navbar-title">联系我们</text>
<view class="navbar-placeholder"></view>
</view>
</view>
<!-- 内容区域 -->
<view class="content-area" :style="{ marginTop: (statusBarHeight + 44) + 'px' }">
<view class="qrcode-card">
<!-- 二维码图片 -->
<view class="qrcode-wrapper">
<image
v-if="butlerQrcode"
class="qrcode-img"
:src="butlerQrcode"
mode="aspectFit"
@click="previewQrcode"
/>
<view v-else class="qrcode-placeholder">
<text>暂无二维码</text>
</view>
</view>
<!-- 保存按钮 -->
<button class="btn-save" @click="saveQrcode" :disabled="!butlerQrcode || saving">
{{ saving ? '保存中...' : '保存图片' }}
</button>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useConfigStore } from '@/store/config.js'
import { getFullImageUrl } from '@/utils/image.js'
import Loading from '@/components/Loading/index.vue'
const configStore = useConfigStore()
//
const statusBarHeight = ref(20)
//
const pageLoading = ref(true)
const saving = ref(false)
//
const butlerQrcode = computed(() => {
const qrcode = configStore.butlerQrcode
return qrcode ? getFullImageUrl(qrcode) : ''
})
//
const getSystemInfo = () => {
try {
const res = uni.getSystemInfoSync()
statusBarHeight.value = res.statusBarHeight || 20
} catch (error) {
console.error('获取系统信息失败:', error)
}
}
//
const handleBack = () => {
uni.navigateBack()
}
//
const previewQrcode = () => {
if (!butlerQrcode.value) return
uni.previewImage({
urls: [butlerQrcode.value]
})
}
//
const saveQrcode = async () => {
if (!butlerQrcode.value || saving.value) return
saving.value = true
try {
//
const downloadRes = await new Promise((resolve, reject) => {
uni.downloadFile({
url: butlerQrcode.value,
success: resolve,
fail: reject
})
})
if (downloadRes.statusCode !== 200) {
throw new Error('下载图片失败')
}
//
await new Promise((resolve, reject) => {
uni.saveImageToPhotosAlbum({
filePath: downloadRes.tempFilePath,
success: resolve,
fail: (err) => {
//
if (err.errMsg.includes('auth deny') || err.errMsg.includes('authorize')) {
uni.showModal({
title: '提示',
content: '需要您授权保存图片到相册',
confirmText: '去设置',
success: (res) => {
if (res.confirm) {
uni.openSetting()
}
}
})
}
reject(err)
}
})
})
uni.showToast({
title: '保存成功',
icon: 'success'
})
} catch (error) {
console.error('保存图片失败:', error)
if (!error.errMsg?.includes('auth')) {
uni.showToast({
title: '保存失败',
icon: 'none'
})
}
} finally {
saving.value = false
}
}
//
const initPage = async () => {
pageLoading.value = true
try {
getSystemInfo()
//
configStore.isLoaded = false
await configStore.loadAppConfig()
} catch (error) {
console.error('初始化页面失败:', error)
} finally {
pageLoading.value = false
}
}
onMounted(() => {
initPage()
})
</script>
<style lang="scss" scoped>
.butler-page {
height: 100vh;
background-color: #f5f6fa;
overflow: hidden;
}
//
.top-bg {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 400rpx;
z-index: 0;
.bg-img {
width: 100%;
height: 100%;
}
}
//
.custom-navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
.navbar-content {
height: 44px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24rpx;
.navbar-back {
width: 80rpx;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
.back-icon {
font-size: 56rpx;
color: #333;
font-weight: 300;
}
}
.navbar-title {
font-size: 34rpx;
font-weight: 600;
color: #333;
}
.navbar-placeholder {
width: 80rpx;
}
}
}
//
.content-area {
position: relative;
z-index: 1;
padding: 40rpx 30rpx;
}
//
.qrcode-card {
background: #fff;
border-radius: 24rpx;
padding: 60rpx 40rpx;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
}
//
.qrcode-wrapper {
width: 480rpx;
height: 480rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 60rpx;
.qrcode-img {
width: 100%;
height: 100%;
}
.qrcode-placeholder {
width: 100%;
height: 100%;
background: #f5f5f5;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
text {
font-size: 28rpx;
color: #999;
}
}
}
//
.btn-save {
width: 320rpx;
height: 88rpx;
line-height: 88rpx;
background: linear-gradient(135deg, #FFBDC2 0%, #FF8A93 100%);
border-radius: 44rpx;
font-size: 32rpx;
color: #fff;
border: none;
&::after {
border: none;
}
&[disabled] {
opacity: 0.6;
}
}
</style>

View File

@ -3,13 +3,29 @@
<!-- 加载状态 -->
<Loading type="page" :loading="loading" />
<!-- 拨打电话底部弹窗 -->
<view class="phone-popup-mask" v-if="showPhonePopup" @click="showPhonePopup = false">
<view class="phone-popup" @click.stop>
<view class="phone-popup-header">
<text class="phone-popup-title">{{ targetNickname }}的联系电话</text>
<text class="phone-popup-close" @click="showPhonePopup = false">×</text>
</view>
<view class="phone-popup-content">
<text class="phone-number">{{ targetUserDetail?.phone || '暂无电话' }}</text>
</view>
<button class="phone-popup-btn" @click="handleMakeCall" :disabled="!targetUserDetail?.phone">
拨打电话
</button>
</view>
</view>
<!-- 自定义导航栏 -->
<view class="custom-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="navbar-content">
<view class="navbar-back" @click="handleBack">
<text class="back-icon"></text>
</view>
<text class="navbar-title">详情资料</text>
<text class="navbar-title">聊天</text>
<view class="navbar-actions">
<text class="action-icon" @click="handleMore">···</text>
<text class="action-icon" @click="handleViewProfile"></text>
@ -21,6 +37,7 @@
<scroll-view
class="content-scroll"
scroll-y
scroll-with-animation
:scroll-into-view="scrollToId"
:style="{ paddingTop: (statusBarHeight + 44) + 'px' }"
@scrolltoupper="loadMoreMessages"
@ -321,7 +338,7 @@
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
import { useUserStore } from '@/store/user.js'
import { useChatStore, MessageType, MessageStatus } from '@/store/chat.js'
import { getMessages, sendMessage, exchangeWeChat, exchangePhoto, respondExchange, uploadVoice } from '@/api/chat.js'
import { getMessages, sendMessage, exchangeWeChat, exchangePhoto, respondExchange, uploadVoice, getOrCreateSession } from '@/api/chat.js'
import { getUserDetail } from '@/api/user.js'
import Loading from '@/components/Loading/index.vue'
import EmojiPicker from '@/components/EmojiPicker/index.vue'
@ -368,6 +385,9 @@ const inputMode = ref('text')
//
const showEmojiPicker = ref(false)
//
const showPhonePopup = ref(false)
// ID
const playingVoiceId = ref(null)
const innerAudioContext = ref(null)
@ -492,8 +512,9 @@ const handleSendMessage = async () => {
inputText.value = ''
const localId = Date.now()
const localMessage = {
id: Date.now(),
id: localId,
sessionId: sessionId.value,
senderId: myUserId.value,
receiverId: targetUserId.value,
@ -516,19 +537,25 @@ const handleSendMessage = async () => {
content
})
if (res && res.code === 0) {
localMessage.status = MessageStatus.SENT
if (res.data && res.data.id) {
localMessage.id = res.data.id
} else if (res.data && res.data.messageId) {
// MessageId
localMessage.id = res.data.messageId
//
const msgIndex = messages.value.findIndex(m => m.id === localId)
if (msgIndex !== -1) {
if (res && res.code === 0) {
messages.value[msgIndex].status = MessageStatus.SENT
if (res.data && res.data.id) {
messages.value[msgIndex].id = res.data.id
} else if (res.data && res.data.messageId) {
messages.value[msgIndex].id = res.data.messageId
}
} else {
messages.value[msgIndex].status = MessageStatus.FAILED
}
} else {
localMessage.status = MessageStatus.FAILED
}
} catch (error) {
localMessage.status = MessageStatus.FAILED
const msgIndex = messages.value.findIndex(m => m.id === localId)
if (msgIndex !== -1) {
messages.value[msgIndex].status = MessageStatus.FAILED
}
uni.showToast({ title: '发送失败', icon: 'none' })
}
}
@ -537,7 +564,10 @@ const handleSendMessage = async () => {
const handleRetryMessage = async (message) => {
if (message.status !== MessageStatus.FAILED) return
message.status = MessageStatus.SENDING
const msgIndex = messages.value.findIndex(m => m.id === message.id)
if (msgIndex === -1) return
messages.value[msgIndex].status = MessageStatus.SENDING
try {
const res = await sendMessage({
@ -548,17 +578,17 @@ const handleRetryMessage = async (message) => {
})
if (res && res.code === 0) {
message.status = MessageStatus.SENT
messages.value[msgIndex].status = MessageStatus.SENT
if (res.data?.messageId) {
message.id = res.data.messageId
messages.value[msgIndex].id = res.data.messageId
}
uni.showToast({ title: '发送成功', icon: 'success' })
} else {
message.status = MessageStatus.FAILED
messages.value[msgIndex].status = MessageStatus.FAILED
uni.showToast({ title: '发送失败', icon: 'none' })
}
} catch (error) {
message.status = MessageStatus.FAILED
messages.value[msgIndex].status = MessageStatus.FAILED
uni.showToast({ title: '发送失败', icon: 'none' })
}
}
@ -593,8 +623,9 @@ const handleSendVoice = async (voiceData) => {
const voiceUrl = uploadRes.data.url
//
const localId = Date.now()
const localMessage = {
id: Date.now(),
id: localId,
sessionId: sessionId.value,
senderId: myUserId.value,
receiverId: targetUserId.value,
@ -621,13 +652,17 @@ const handleSendVoice = async (voiceData) => {
uni.hideLoading()
if (res && res.code === 0) {
localMessage.status = MessageStatus.SENT
if (res.data && res.data.messageId) {
localMessage.id = res.data.messageId
//
const msgIndex = messages.value.findIndex(m => m.id === localId)
if (msgIndex !== -1) {
if (res && res.code === 0) {
messages.value[msgIndex].status = MessageStatus.SENT
if (res.data && res.data.messageId) {
messages.value[msgIndex].id = res.data.messageId
}
} else {
messages.value[msgIndex].status = MessageStatus.FAILED
}
} else {
localMessage.status = MessageStatus.FAILED
}
} else {
uni.hideLoading()
@ -823,7 +858,23 @@ const previewPhotos = (photos, index) => {
//
const handleCall = () => {
uni.showToast({ title: '请通过微信联系对方', icon: 'none' })
showPhonePopup.value = true
}
//
const handleMakeCall = () => {
const phone = targetUserDetail.value?.phone
if (!phone) {
uni.showToast({ title: '暂无电话号码', icon: 'none' })
return
}
uni.makePhoneCall({
phoneNumber: phone,
fail: (err) => {
console.error('拨打电话失败:', err)
}
})
}
//
@ -865,7 +916,11 @@ const previewImage = (url) => {
}
const scrollToBottom = () => {
scrollToId.value = 'bottom-anchor'
//
scrollToId.value = ''
nextTick(() => {
scrollToId.value = 'bottom-anchor'
})
}
const handleBack = () => {
@ -908,13 +963,36 @@ onMounted(async () => {
//
loadTargetUserDetail()
// sessionIdtargetUserId
if (!sessionId.value && targetUserId.value) {
try {
uni.showLoading({ title: '加载中...' })
const res = await getOrCreateSession(targetUserId.value)
uni.hideLoading()
if (res && res.code === 0 && res.data) {
sessionId.value = res.data
} else {
uni.showToast({ title: res?.message || '创建会话失败', icon: 'none' })
setTimeout(() => {
uni.navigateBack()
}, 1500)
return
}
} catch (error) {
uni.hideLoading()
console.error('获取会话失败:', error)
uni.showToast({ title: '网络错误', icon: 'none' })
setTimeout(() => {
uni.navigateBack()
}, 1500)
return
}
}
if (sessionId.value) {
chatStore.setCurrentSession(sessionId.value)
loadMessages()
} else if (targetUserId.value) {
sessionId.value = targetUserId.value
chatStore.setCurrentSession(sessionId.value)
loadMessages()
} else {
uni.showToast({ title: '参数错误', icon: 'none' })
setTimeout(() => {
@ -1061,7 +1139,8 @@ onUnmounted(() => {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f6fa;
background-color: #f8f8f8;
overflow: hidden;
}
//
@ -1116,7 +1195,9 @@ onUnmounted(() => {
//
.content-scroll {
flex: 1;
height: 0;
padding-bottom: 280rpx;
background-color: #f8f8f8;
}
//
@ -1713,4 +1794,76 @@ onUnmounted(() => {
transform: scaleY(1.5);
}
}
//
.phone-popup-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
display: flex;
align-items: flex-end;
justify-content: center;
}
.phone-popup {
width: 100%;
background: #fff;
border-radius: 24rpx 24rpx 0 0;
padding: 40rpx;
padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
.phone-popup-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 40rpx;
.phone-popup-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.phone-popup-close {
font-size: 48rpx;
color: #999;
line-height: 1;
}
}
.phone-popup-content {
text-align: center;
padding: 40rpx 0;
.phone-number {
font-size: 56rpx;
font-weight: 600;
color: #333;
letter-spacing: 4rpx;
}
}
.phone-popup-btn {
width: 100%;
height: 96rpx;
line-height: 96rpx;
background: #4cd964;
color: #fff;
font-size: 34rpx;
border-radius: 48rpx;
border: none;
&::after {
border: none;
}
&[disabled] {
opacity: 0.5;
}
}
}
</style>

View File

@ -12,6 +12,16 @@
:buttonText="dailyPopup?.buttonText" :linkUrl="dailyPopup?.linkUrl" @close="handleCloseDailyPopup"
@confirm="handleDailyPopupConfirm" />
<!-- 解锁确认弹窗 -->
<Popup
:visible="showUnlockPopup"
type="unlock"
:remainingQuota="remainingUnlockQuota"
@close="handleCloseUnlockPopup"
@unlock="handleConfirmUnlock"
@goMember="handleGoMember"
/>
<!-- 会员广告条 - 固定在底部 -->
<view class="member-ad-section" v-if="showMemberAd">
<view class="member-ad-bar" :style="memberAdBgStyle">
@ -128,6 +138,10 @@
import {
getRecommend
} from '@/api/user.js'
import {
checkUnlock,
unlock
} from '@/api/interact.js'
import {
getFullImageUrl
} from '@/utils/image.js'
@ -165,6 +179,11 @@
//
const recommendList = ref([])
//
const showUnlockPopup = ref(false)
const unlockTargetUserId = ref(0)
const remainingUnlockQuota = ref(0)
// - URL
const banners = computed(() => {
return configStore.banners.map(banner => ({
@ -372,7 +391,7 @@
}
//
const handleUserContact = (userId) => {
const handleUserContact = async (userId) => {
if (!userStore.isLoggedIn) {
uni.showModal({
title: '提示',
@ -405,9 +424,77 @@
return
}
uni.navigateTo({
url: `/pages/chat/index?targetUserId=${userId}`
})
//
try {
uni.showLoading({ title: '加载中...' })
const res = await checkUnlock(userId)
uni.hideLoading()
if (res && res.code === 0 && res.data) {
if (res.data.isUnlocked) {
//
uni.navigateTo({
url: `/pages/chat/index?targetUserId=${userId}`
})
} else {
//
unlockTargetUserId.value = userId
remainingUnlockQuota.value = res.data.remainingUnlockQuota || 0
showUnlockPopup.value = true
}
} else {
uni.showToast({ title: '检查解锁状态失败', icon: 'none' })
}
} catch (error) {
uni.hideLoading()
console.error('检查解锁状态失败:', error)
uni.showToast({ title: '网络错误', icon: 'none' })
}
}
//
const handleCloseUnlockPopup = () => {
showUnlockPopup.value = false
}
//
const handleConfirmUnlock = async () => {
if (remainingUnlockQuota.value <= 0) {
showUnlockPopup.value = false
uni.navigateTo({ url: '/pages/member/index' })
return
}
try {
uni.showLoading({ title: '解锁中...' })
const unlockRes = await unlock(unlockTargetUserId.value)
uni.hideLoading()
if (unlockRes && unlockRes.code === 0) {
showUnlockPopup.value = false
uni.showToast({ title: '解锁成功', icon: 'success' })
setTimeout(() => {
uni.navigateTo({
url: `/pages/chat/index?targetUserId=${unlockTargetUserId.value}`
})
}, 1000)
} else {
uni.showToast({
title: unlockRes?.message || '解锁失败',
icon: 'none'
})
}
} catch (error) {
uni.hideLoading()
console.error('解锁失败:', error)
uni.showToast({ title: '解锁失败', icon: 'none' })
}
}
//
const handleGoMember = () => {
showUnlockPopup.value = false
uni.navigateTo({ url: '/pages/member/index' })
}
// -
@ -446,6 +533,8 @@
showDailyPopup,
dailyPopup,
recommendList,
showUnlockPopup,
remainingUnlockQuota,
initPage,
loadRecommendList,
handleSearchClick,
@ -458,6 +547,9 @@
handleDailyPopupConfirm,
handleUserClick,
handleUserContact,
handleCloseUnlockPopup,
handleConfirmUnlock,
handleGoMember,
handleScrollToLower,
handleRefresh
}

File diff suppressed because it is too large Load Diff

View File

@ -50,31 +50,37 @@
<!-- 互动统计区域 -->
<view class="interact-section">
<view class="interact-grid">
<view class="interact-item" @click="navigateTo('/pages/interact/viewedMe')">
<view class="interact-icon viewed">
<image src="/static/ic_seen.png" mode="aspectFit" class="icon-img" />
</view>
<text class="interact-label">看过我</text>
<view class="interact-badge" v-if="interactCounts.viewedMe > 0">
<text>+{{ interactCounts.viewedMe }}</text>
<view class="interact-item viewed" @click="navigateTo('/pages/interact/viewedMe')">
<view class="item-card">
<view class="icon-box">
<image src="/static/ic_seen.png" mode="aspectFit" class="icon-img" />
</view>
<text class="item-label">看过我</text>
<view class="item-count" v-if="interactCounts.viewedMe > 0">
<text>+{{ interactCounts.viewedMe }}</text>
</view>
</view>
</view>
<view class="interact-item" @click="navigateTo('/pages/interact/favoritedMe')">
<view class="interact-icon favorited">
<image src="/static/ic_collection.png" mode="aspectFit" class="icon-img" />
</view>
<text class="interact-label">收藏我</text>
<view class="interact-badge" v-if="interactCounts.favoritedMe > 0">
<text>+{{ interactCounts.favoritedMe }}</text>
<view class="interact-item favorited" @click="navigateTo('/pages/interact/favoritedMe')">
<view class="item-card">
<view class="icon-box">
<image src="/static/ic_collection.png" mode="aspectFit" class="icon-img" />
</view>
<text class="item-label">收藏我</text>
<view class="item-count" v-if="interactCounts.favoritedMe > 0">
<text>+{{ interactCounts.favoritedMe }}</text>
</view>
</view>
</view>
<view class="interact-item" @click="navigateTo('/pages/interact/unlockedMe')">
<view class="interact-icon unlocked">
<image src="/static/ic_unlock.png" mode="aspectFit" class="icon-img" />
</view>
<text class="interact-label">解锁我</text>
<view class="interact-badge" v-if="interactCounts.unlockedMe > 0">
<text>+{{ interactCounts.unlockedMe }}</text>
<view class="interact-item unlocked" @click="navigateTo('/pages/interact/unlockedMe')">
<view class="item-card">
<view class="icon-box">
<image src="/static/ic_unlock.png" mode="aspectFit" class="icon-img" />
</view>
<text class="item-label">解锁我</text>
<view class="item-count" v-if="interactCounts.unlockedMe > 0">
<text>+{{ interactCounts.unlockedMe }}</text>
</view>
</view>
</view>
</view>
@ -125,6 +131,11 @@
</view>
</view>
</view>
<!-- ICP备案号 -->
<view class="icp-section">
<text class="icp-text">备案号苏ICP备2026001086号-1</text>
</view>
</view>
</template>
@ -251,7 +262,7 @@ export default {
//
const handleButler = () => {
uni.showToast({ title: '功能开发中', icon: 'none' })
uni.navigateTo({ url: '/pages/butler/index' })
}
//
@ -526,76 +537,83 @@ export default {
.interact-grid {
display: flex;
justify-content: space-between;
flex-direction: row;
justify-content: space-around;
.interact-item {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
.interact-icon {
width: 120rpx;
height: 120rpx;
.item-card {
display: flex;
flex-direction: column;
align-items: center;
width: 150rpx;
height: 132rpx;
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16rpx;
.icon-img {
width: 56rpx;
height: 56rpx;
.icon-box {
width: 64rpx;
height: 64rpx;
border-radius: 20rpx;
margin-top: -18rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 6rpx;
.icon-img {
width: 64rpx;
height: 64rpx;
}
}
&.viewed {
background: linear-gradient(135deg, #e8e0ff 0%, #d4c4ff 100%);
}
&.favorited {
background: linear-gradient(135deg, #ffe0e8 0%, #ffc4d4 100%);
}
&.unlocked {
background: linear-gradient(135deg, #fff3e0 0%, #ffe4c4 100%);
}
}
.interact-label {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
.interact-badge {
position: absolute;
top: -8rpx;
right: -8rpx;
min-width: 44rpx;
height: 44rpx;
padding: 0 12rpx;
border-radius: 22rpx;
display: flex;
align-items: center;
justify-content: center;
text {
font-size: 24rpx;
color: #fff;
.item-label {
font-size: 28rpx;
color: #333;
font-weight: 500;
margin-bottom: 6rpx;
}
.item-count {
min-width: 56rpx;
height: 40rpx;
padding: 5rpx 0rpx;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
text {
font-size: 20rpx;
color: #fff;
font-weight: 600;
}
}
}
&:nth-child(1) .interact-badge {
background: #9b7bff;
// -
&.viewed .item-card {
background: linear-gradient(0deg, rgba(237, 213, 255, 0.1) 0%, #E0D8FF 100%);
.item-count {
background: #8b5cf6;
}
}
&:nth-child(2) .interact-badge {
background: #ff6b8a;
// -
&.favorited .item-card {
background: linear-gradient(0deg, rgba(255, 213, 240, 0.1) 0%, #FFC4D7 100%);
.item-count {
background: #fb7185;
}
}
&:nth-child(3) .interact-badge {
background: #ffb347;
// -
&.unlocked .item-card {
background: linear-gradient(0deg, rgba(255, 239, 213, 0.1) 0%, #FFF5D8 100%);
.item-count {
background: #f59e0b;
}
}
}
}
@ -689,4 +707,17 @@ export default {
}
}
}
// ICP
.icp-section {
position: relative;
z-index: 1;
padding: 40rpx 32rpx 60rpx;
text-align: center;
.icp-text {
font-size: 24rpx;
color: #999;
}
}
</style>

View File

@ -21,6 +21,22 @@
@goMember="handleGoMember"
/>
<!-- 拨打电话底部弹窗 -->
<view class="phone-popup-mask" v-if="showPhonePopup" @click="showPhonePopup = false">
<view class="phone-popup" @click.stop>
<view class="phone-popup-header">
<text class="phone-popup-title">{{ userDetail?.nickname }}的联系电话</text>
<text class="phone-popup-close" @click="showPhonePopup = false">×</text>
</view>
<view class="phone-popup-content">
<text class="phone-number">{{ userDetail?.phone || '暂无电话' }}</text>
</view>
<button class="phone-popup-btn" @click="handleMakeCall" :disabled="!userDetail?.phone">
拨打电话
</button>
</view>
</view>
<!-- 自定义导航栏 -->
<view class="custom-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="navbar-content">
@ -282,6 +298,7 @@ const isUnlocked = ref(false)
const remainingUnlockQuota = ref(0)
const showProfilePopup = ref(false)
const showUnlockPopup = ref(false)
const showPhonePopup = ref(false)
//
const getSystemInfo = () => {
@ -624,7 +641,25 @@ const handleCall = () => {
showUnlockPopup.value = true
return
}
uni.showToast({ title: '请通过微信联系对方', icon: 'none' })
//
showPhonePopup.value = true
}
//
const handleMakeCall = () => {
const phone = userDetail.value?.phone
if (!phone) {
uni.showToast({ title: '暂无电话号码', icon: 'none' })
return
}
uni.makePhoneCall({
phoneNumber: phone,
fail: (err) => {
console.error('拨打电话失败:', err)
}
})
}
//
@ -672,6 +707,7 @@ onShareAppMessage(() => {
.profile-detail-page {
min-height: 100vh;
background-color: #f5f6fa;
overflow: hidden;
}
//
@ -718,12 +754,13 @@ onShareAppMessage(() => {
//
.content-scroll {
height: 100vh;
padding-bottom: 160rpx;
height: calc(100vh - 160rpx);
box-sizing: border-box;
}
.content-wrapper {
padding: 24rpx;
padding-bottom: 40rpx;
}
//
@ -1114,4 +1151,76 @@ onShareAppMessage(() => {
color: #fff;
}
}
//
.phone-popup-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
display: flex;
align-items: flex-end;
justify-content: center;
}
.phone-popup {
width: 100%;
background: #fff;
border-radius: 24rpx 24rpx 0 0;
padding: 40rpx;
padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
.phone-popup-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 40rpx;
.phone-popup-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.phone-popup-close {
font-size: 48rpx;
color: #999;
line-height: 1;
}
}
.phone-popup-content {
text-align: center;
padding: 40rpx 0;
.phone-number {
font-size: 56rpx;
font-weight: 600;
color: #333;
letter-spacing: 4rpx;
}
}
.phone-popup-btn {
width: 100%;
height: 96rpx;
line-height: 96rpx;
background: #4cd964;
color: #fff;
font-size: 34rpx;
border-radius: 48rpx;
border: none;
&::after {
border: none;
}
&[disabled] {
opacity: 0.5;
}
}
}
</style>

View File

@ -7,13 +7,16 @@ import { defineStore } from 'pinia'
/**
* 消息类型枚举
* 与后端 MessageType 枚举保持一致
*/
export const MessageType = {
TEXT: 1, // 文本消息
VOICE: 2, // 语音消息
IMAGE: 3, // 图片消息
EXCHANGE_WECHAT: 4, // 交换微信
EXCHANGE_PHOTO: 5 // 交换照片
TEXT: 1, // 文本消息
VOICE: 2, // 语音消息
IMAGE: 3, // 图片消息
EXCHANGE_WECHAT: 4, // 交换微信请求
EXCHANGE_WECHAT_RESULT: 5, // 交换微信结果
EXCHANGE_PHOTO: 6, // 交换照片请求
EXCHANGE_PHOTO_RESULT: 7 // 交换照片结果
}
/**

View File

@ -33,6 +33,7 @@ export const useConfigStore = defineStore('config', {
// 系统配置
defaultAvatar: getDefaultAvatar() || '/static/logo.png',
searchBanner: '',
butlerQrcode: '', // 管家指导二维码
// 弹窗配置
dailyPopup: null,
@ -77,6 +78,7 @@ export const useConfigStore = defineStore('config', {
setDefaultAvatar(config.defaultAvatar)
}
this.searchBanner = config.searchBanner || ''
this.butlerQrcode = config.butlerQrcode || ''
// 弹窗配置
this.dailyPopup = config.dailyPopup || null

View File

@ -173,6 +173,34 @@ public class AdminConfigController : ControllerBase
var result = await _configService.SetSearchBannerAsync(request.ImageUrl);
return result ? ApiResponse.Success("设置成功") : ApiResponse.Error(40001, "设置失败");
}
/// <summary>
/// 获取管家二维码
/// </summary>
[HttpGet("butlerQrcode")]
public async Task<ApiResponse<ButlerQrcodeResponse>> GetButlerQrcode()
{
var imageUrl = await _configService.GetButlerQrcodeAsync();
return ApiResponse<ButlerQrcodeResponse>.Success(new ButlerQrcodeResponse
{
ImageUrl = imageUrl
});
}
/// <summary>
/// 设置管家二维码
/// </summary>
[HttpPost("butlerQrcode")]
public async Task<ApiResponse> SetButlerQrcode([FromBody] SetButlerQrcodeRequest request)
{
if (string.IsNullOrWhiteSpace(request.ImageUrl))
{
return ApiResponse.Error(40001, "图片URL不能为空");
}
var result = await _configService.SetButlerQrcodeAsync(request.ImageUrl);
return result ? ApiResponse.Success("设置成功") : ApiResponse.Error(40001, "设置失败");
}
}
/// <summary>
@ -262,3 +290,25 @@ public class SetSearchBannerRequest
/// </summary>
public string ImageUrl { get; set; } = string.Empty;
}
/// <summary>
/// 管家二维码响应
/// </summary>
public class ButlerQrcodeResponse
{
/// <summary>
/// 图片URL
/// </summary>
public string? ImageUrl { get; set; }
}
/// <summary>
/// 设置管家二维码请求
/// </summary>
public class SetButlerQrcodeRequest
{
/// <summary>
/// 图片URL
/// </summary>
public string ImageUrl { get; set; } = string.Empty;
}

View File

@ -111,6 +111,20 @@ public class AdminUserController : ControllerBase
var result = await _adminUserService.DeleteUserAsync(id, adminId);
return result ? ApiResponse.Success("删除成功") : ApiResponse.Error(40001, "删除失败");
}
/// <summary>
/// 更新用户联系次数
/// </summary>
/// <param name="id">用户ID</param>
/// <param name="request">联系次数更新请求</param>
/// <returns>操作结果</returns>
[HttpPut("{id}/contact-count")]
public async Task<ApiResponse> UpdateContactCount(long id, [FromBody] UpdateContactCountRequest request)
{
var adminId = GetCurrentAdminId();
var result = await _adminUserService.UpdateContactCountAsync(id, request.ContactCount, adminId);
return result ? ApiResponse.Success("更新成功") : ApiResponse.Error(40001, "更新失败");
}
}
/// <summary>
@ -128,3 +142,14 @@ public class CreateTestUsersRequest
/// </summary>
public int? Gender { get; set; }
}
/// <summary>
/// 更新联系次数请求
/// </summary>
public class UpdateContactCountRequest
{
/// <summary>
/// 联系次数
/// </summary>
public int ContactCount { get; set; }
}

View File

@ -293,6 +293,29 @@ public class ChatController : ControllerBase
return ApiResponse<int>.Success(count);
}
/// <summary>
/// 获取或创建会话
/// </summary>
/// <param name="targetUserId">目标用户ID</param>
/// <returns>会话ID</returns>
[HttpGet("session")]
public async Task<ApiResponse<long>> GetOrCreateSession([FromQuery] long targetUserId)
{
if (targetUserId <= 0)
{
return ApiResponse<long>.Error(ErrorCodes.InvalidParameter, "目标用户ID无效");
}
var userId = GetCurrentUserId();
if (userId == targetUserId)
{
return ApiResponse<long>.Error(ErrorCodes.InvalidParameter, "不能与自己创建会话");
}
var sessionId = await _chatService.GetOrCreateSessionAsync(userId, targetUserId);
return ApiResponse<long>.Success(sessionId);
}
/// <summary>
/// 获取当前用户ID
/// </summary>

View File

@ -226,6 +226,30 @@ public class InteractController : ControllerBase
return ApiResponse<PagedResult<UnlockRecordResponse>>.Success(result);
}
/// <summary>
/// 检查是否已解锁目标用户
/// </summary>
/// <param name="targetUserId">目标用户ID</param>
/// <returns>解锁状态</returns>
[HttpGet("checkUnlock")]
public async Task<ApiResponse<CheckUnlockResponse>> CheckUnlock([FromQuery] long targetUserId)
{
if (targetUserId <= 0)
{
return ApiResponse<CheckUnlockResponse>.Error(ErrorCodes.InvalidParameter, "目标用户ID无效");
}
var userId = GetCurrentUserId();
var isUnlocked = await _interactService.IsUnlockedAsync(userId, targetUserId);
var remainingQuota = await _interactService.GetRemainingUnlockQuotaAsync(userId);
return ApiResponse<CheckUnlockResponse>.Success(new CheckUnlockResponse
{
IsUnlocked = isUnlocked,
RemainingUnlockQuota = remainingQuota
});
}
/// <summary>
/// 获取当前用户ID
/// </summary>
@ -256,3 +280,19 @@ public class FavoriteResponse
/// </summary>
public string? Message { get; set; }
}
/// <summary>
/// 检查解锁状态响应
/// </summary>
public class CheckUnlockResponse
{
/// <summary>
/// 是否已解锁
/// </summary>
public bool IsUnlocked { get; set; }
/// <summary>
/// 剩余解锁次数
/// </summary>
public int RemainingUnlockQuota { get; set; }
}

View File

@ -52,4 +52,13 @@ public interface IAdminUserService
/// <param name="adminId">操作管理员ID</param>
/// <returns>是否成功</returns>
Task<bool> DeleteUserAsync(long userId, long adminId);
/// <summary>
/// 更新用户联系次数
/// </summary>
/// <param name="userId">用户ID</param>
/// <param name="contactCount">联系次数</param>
/// <param name="adminId">操作管理员ID</param>
/// <returns>是否成功</returns>
Task<bool> UpdateContactCountAsync(long userId, int contactCount, long adminId);
}

View File

@ -89,6 +89,11 @@ public class AppConfigResponse
/// </summary>
public string? SearchBanner { get; set; }
/// <summary>
/// 管家指导二维码URL
/// </summary>
public string? ButlerQrcode { get; set; }
/// <summary>
/// 每日弹窗配置
/// </summary>

View File

@ -69,4 +69,14 @@ public interface ISystemConfigService
/// 设置搜索页Banner图URL
/// </summary>
Task<bool> SetSearchBannerAsync(string imageUrl);
/// <summary>
/// 获取管家指导二维码URL
/// </summary>
Task<string?> GetButlerQrcodeAsync();
/// <summary>
/// 设置管家指导二维码URL
/// </summary>
Task<bool> SetButlerQrcodeAsync(string imageUrl);
}

View File

@ -439,6 +439,32 @@ public class AdminUserService : IAdminUserService
return true;
}
/// <inheritdoc />
public async Task<bool> UpdateContactCountAsync(long userId, int contactCount, long adminId)
{
var user = await _userRepository.GetByIdAsync(userId);
if (user == null || user.IsDeleted)
{
throw new BusinessException(ErrorCodes.UserNotFound, "用户不存在");
}
if (contactCount < 0)
{
throw new BusinessException(ErrorCodes.InvalidParameter, "联系次数不能为负数");
}
var oldCount = user.ContactCount;
user.ContactCount = contactCount;
user.UpdateTime = DateTime.Now;
var result = await _userRepository.UpdateAsync(user);
_logger.LogInformation("管理员更新用户联系次数: AdminId={AdminId}, UserId={UserId}, OldCount={OldCount}, NewCount={NewCount}",
adminId, userId, oldCount, contactCount);
return result > 0;
}
#region Private Helper Methods
private static AdminUserListDto MapToUserListDto(User user, UserProfile? profile)

View File

@ -47,6 +47,7 @@ public class ConfigService : IConfigService
var kingKongs = await GetKingKongsAsync();
var defaultAvatar = await _systemConfigService.GetDefaultAvatarAsync();
var searchBanner = await _systemConfigService.GetSearchBannerAsync();
var butlerQrcode = await _systemConfigService.GetButlerQrcodeAsync();
var dailyPopup = await GetPopupConfigAsync(1); // 每日弹窗
var memberAdPopup = await GetPopupConfigAsync(3); // 会员广告弹窗
@ -56,6 +57,7 @@ public class ConfigService : IConfigService
KingKongs = kingKongs,
DefaultAvatar = defaultAvatar,
SearchBanner = searchBanner,
ButlerQrcode = butlerQrcode,
DailyPopup = dailyPopup,
MemberAdPopup = memberAdPopup
};

View File

@ -473,8 +473,10 @@ public class InteractService : IInteractService
/// <inheritdoc />
public async Task<bool> IsUnlockedAsync(long userId, long targetUserId)
{
// 检查双向解锁:只要任意一方解锁了对方,双方都可以聊天
return await _unlockRepository.ExistsAsync(u =>
u.UserId == userId && u.TargetUserId == targetUserId);
(u.UserId == userId && u.TargetUserId == targetUserId) ||
(u.UserId == targetUserId && u.TargetUserId == userId));
}
/// <inheritdoc />

View File

@ -38,6 +38,11 @@ public class SystemConfigService : ISystemConfigService
/// </summary>
public const string SearchBannerKey = "search_banner";
/// <summary>
/// 管家二维码配置键
/// </summary>
public const string ButlerQrcodeKey = "butler_qrcode";
public SystemConfigService(
IRepository<SystemConfig> configRepository,
ILogger<SystemConfigService> logger)
@ -160,4 +165,16 @@ public class SystemConfigService : ISystemConfigService
{
return await SetConfigValueAsync(SearchBannerKey, imageUrl, "搜索页Banner图URL");
}
/// <inheritdoc />
public async Task<string?> GetButlerQrcodeAsync()
{
return await GetConfigValueAsync(ButlerQrcodeKey);
}
/// <inheritdoc />
public async Task<bool> SetButlerQrcodeAsync(string imageUrl)
{
return await SetConfigValueAsync(ButlerQrcodeKey, imageUrl, "管家指导二维码URL");
}
}