聊天,会员
This commit is contained in:
parent
de2b59ca90
commit
0e84a4fe84
|
|
@ -16,7 +16,8 @@ import type { PopupItem, UpdatePopupRequest } from '@/types/popup.d'
|
|||
const popupTypes = [
|
||||
{ type: 1, name: '每日首次弹窗', description: '用户每天首次打开小程序时展示' },
|
||||
{ type: 2, name: '服务号关注弹窗', description: '引导用户关注公众号' },
|
||||
{ type: 3, name: '会员广告弹窗', description: '会员推广广告条,显示在首页底部', showDisplayMode: true }
|
||||
{ type: 3, name: '会员广告弹窗', description: '会员推广广告条,显示在首页底部', showDisplayMode: true },
|
||||
{ type: 4, name: '订阅消息提醒条', description: '引导用户开启消息通知,显示在首页底部' }
|
||||
]
|
||||
|
||||
// 显示模式选项
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { ElMessage, ElMessageBox } from 'element-plus'
|
|||
import { ArrowLeft } from '@element-plus/icons-vue'
|
||||
import StatusTag from '@/components/StatusTag/index.vue'
|
||||
import { getUserDetail, updateUserStatus, updateContactCount, updateMemberLevel, cancelRealName } from '@/api/user'
|
||||
import { getMemberTierList } from '@/api/memberTier'
|
||||
import { getFullImageUrl } from '@/utils/image'
|
||||
import type { UserDetail } from '@/types/user.d'
|
||||
|
||||
|
|
@ -106,14 +107,26 @@ const handleEditContactCount = async () => {
|
|||
}
|
||||
}
|
||||
|
||||
// 会员等级选项
|
||||
const memberLevelOptions = [
|
||||
{ value: 0, label: '非会员' },
|
||||
{ value: 1, label: '不限时会员' },
|
||||
{ value: 2, label: '诚意会员' },
|
||||
{ value: 3, label: '家庭版会员' },
|
||||
{ value: 4, label: '限时会员' }
|
||||
]
|
||||
// 会员等级选项 - 从后台配置动态获取
|
||||
const memberLevelOptions = ref([
|
||||
{ value: 0, label: '非会员' }
|
||||
])
|
||||
|
||||
// 加载会员等级配置
|
||||
const loadMemberTierOptions = async () => {
|
||||
try {
|
||||
const res = await getMemberTierList()
|
||||
const tiers = res.data || res || []
|
||||
if (Array.isArray(tiers) && tiers.length > 0) {
|
||||
memberLevelOptions.value = [
|
||||
{ value: 0, label: '非会员' },
|
||||
...tiers.map((t: any) => ({ value: t.level, label: t.name }))
|
||||
]
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载会员等级配置失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 修改会员等级对话框
|
||||
const memberLevelDialogVisible = ref(false)
|
||||
|
|
@ -257,6 +270,7 @@ const formatIncome = (min: number | undefined, max: number | undefined) => {
|
|||
|
||||
onMounted(() => {
|
||||
fetchUserDetail()
|
||||
loadMemberTierOptions()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import SearchForm from '@/components/SearchForm/index.vue'
|
|||
import Pagination from '@/components/Pagination/index.vue'
|
||||
import StatusTag from '@/components/StatusTag/index.vue'
|
||||
import { getUserList, updateUserStatus, createTestUsers, deleteUser } from '@/api/user'
|
||||
import { getMemberTierList } from '@/api/memberTier'
|
||||
import { getFullImageUrl } from '@/utils/image'
|
||||
import type { UserListItem, UserQueryParams } from '@/types/user.d'
|
||||
|
||||
|
|
@ -54,15 +55,28 @@ const memberOptions = [
|
|||
{ label: '否', value: false }
|
||||
]
|
||||
|
||||
// 会员等级选项
|
||||
const memberLevelOptions = [
|
||||
// 会员等级选项 - 从后台配置动态获取
|
||||
const memberLevelOptions = ref([
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '非会员', value: 0 },
|
||||
{ label: '不限时', value: 1 },
|
||||
{ label: '诚意', value: 2 },
|
||||
{ label: '家庭版', value: 3 },
|
||||
{ label: '限时', value: 4 }
|
||||
]
|
||||
{ label: '非会员', value: 0 }
|
||||
])
|
||||
|
||||
// 加载会员等级配置
|
||||
const loadMemberTierOptions = async () => {
|
||||
try {
|
||||
const res = await getMemberTierList()
|
||||
const tiers = res.data || res || []
|
||||
if (Array.isArray(tiers) && tiers.length > 0) {
|
||||
memberLevelOptions.value = [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '非会员', value: 0 },
|
||||
...tiers.map((t: any) => ({ label: t.name, value: t.level }))
|
||||
]
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载会员等级配置失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 实名认证选项
|
||||
const realNameOptions = [
|
||||
|
|
@ -213,8 +227,8 @@ const handleOpenTestUserDialog = () => {
|
|||
|
||||
// 创建测试用户
|
||||
const handleCreateTestUsers = async () => {
|
||||
if (testUserForm.count < 1 || testUserForm.count > 50) {
|
||||
ElMessage.warning('创建数量必须在1-50之间')
|
||||
if (testUserForm.count < 1 || testUserForm.count > 100) {
|
||||
ElMessage.warning('创建数量必须在1-100之间')
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -233,6 +247,7 @@ const handleCreateTestUsers = async () => {
|
|||
|
||||
onMounted(() => {
|
||||
fetchUserList()
|
||||
loadMemberTierOptions()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
@ -559,9 +574,10 @@ onMounted(() => {
|
|||
<el-input-number
|
||||
v-model="testUserForm.count"
|
||||
:min="1"
|
||||
:max="50"
|
||||
:max="100"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<div style="color: #909399; font-size: 12px; margin-top: 4px;">单次最多创建100个</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="性别">
|
||||
<el-radio-group v-model="testUserForm.gender">
|
||||
|
|
|
|||
|
|
@ -4,107 +4,137 @@
|
|||
<view
|
||||
class="voice-btn"
|
||||
:class="{ recording: isRecording }"
|
||||
@touchstart="handleTouchStart"
|
||||
@touchend="handleTouchEnd"
|
||||
@touchcancel="handleTouchCancel"
|
||||
@touchstart.prevent="handleTouchStart"
|
||||
@touchmove.prevent="handleTouchMove"
|
||||
@touchend.prevent="handleTouchEnd"
|
||||
@touchcancel.prevent="handleTouchCancel"
|
||||
>
|
||||
<text>{{ isRecording ? '松开发送' : '按住说话' }}</text>
|
||||
<text>{{ btnText }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 录音提示弹窗 -->
|
||||
<view class="voice-modal" v-if="isRecording">
|
||||
<view class="voice-modal-content">
|
||||
<view class="voice-modal" v-if="showModal">
|
||||
<view class="voice-modal-content" :class="{ cancel: isCancelling }">
|
||||
<view class="voice-icon">
|
||||
<view class="voice-wave" :class="{ active: isRecording }"></view>
|
||||
<text class="icon">🎤</text>
|
||||
<view class="voice-wave" :class="{ active: isRecording && !isCancelling }"></view>
|
||||
<text class="icon">{{ isCancelling ? '↩' : '🎤' }}</text>
|
||||
</view>
|
||||
<text class="voice-tip">{{ recordingTip }}</text>
|
||||
<text class="voice-time">{{ recordingTime }}s</text>
|
||||
<text class="voice-time" v-if="!isCancelling">{{ recordingTime }}s</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onUnmounted } from 'vue'
|
||||
import { ref, computed, onUnmounted } from 'vue'
|
||||
|
||||
const emit = defineEmits(['send'])
|
||||
|
||||
const isRecording = ref(false)
|
||||
const showModal = ref(false)
|
||||
const isCancelling = ref(false)
|
||||
const recordingTime = ref(0)
|
||||
const recordingTip = ref('正在录音...')
|
||||
const recorderManager = ref(null)
|
||||
const recordingTimer = ref(null)
|
||||
const tempFilePath = ref('')
|
||||
const startY = ref(0)
|
||||
|
||||
// 是否是用户主动取消(上滑取消)
|
||||
let cancelled = false
|
||||
|
||||
const btnText = computed(() => {
|
||||
if (isRecording.value && isCancelling.value) return '松开取消'
|
||||
if (isRecording.value) return '松开发送'
|
||||
return '按住说话'
|
||||
})
|
||||
|
||||
const recordingTip = computed(() => {
|
||||
if (isCancelling.value) return '松开手指,取消发送'
|
||||
return '正在录音...'
|
||||
})
|
||||
|
||||
// 初始化录音管理器
|
||||
const initRecorder = () => {
|
||||
if (recorderManager.value) return
|
||||
|
||||
recorderManager.value = uni.getRecorderManager()
|
||||
|
||||
// 录音开始
|
||||
recorderManager.value.onStart(() => {
|
||||
console.log('[VoiceRecorder] 录音开始')
|
||||
isRecording.value = true
|
||||
showModal.value = true
|
||||
recordingTime.value = 0
|
||||
recordingTip.value = '正在录音...'
|
||||
cancelled = false
|
||||
|
||||
// 开始计时
|
||||
recordingTimer.value = setInterval(() => {
|
||||
recordingTime.value++
|
||||
|
||||
// 最长60秒
|
||||
if (recordingTime.value >= 60) {
|
||||
handleTouchEnd()
|
||||
stopRecording()
|
||||
}
|
||||
}, 1000)
|
||||
})
|
||||
|
||||
// 录音结束
|
||||
recorderManager.value.onStop((res) => {
|
||||
console.log('[VoiceRecorder] 录音结束:', res)
|
||||
clearInterval(recordingTimer.value)
|
||||
console.log('[VoiceRecorder] 录音结束:', res, '已取消:', cancelled)
|
||||
clearTimer()
|
||||
isRecording.value = false
|
||||
showModal.value = false
|
||||
isCancelling.value = false
|
||||
|
||||
tempFilePath.value = res.tempFilePath
|
||||
const duration = Math.ceil(res.duration / 1000)
|
||||
|
||||
// 录音时长小于1秒,提示太短
|
||||
if (duration < 1) {
|
||||
uni.showToast({
|
||||
title: '录音时间太短',
|
||||
icon: 'none'
|
||||
})
|
||||
// 如果是取消的,不发送
|
||||
if (cancelled) {
|
||||
cancelled = false
|
||||
return
|
||||
}
|
||||
|
||||
const duration = Math.ceil((res.duration || 0) / 1000)
|
||||
|
||||
if (duration < 1) {
|
||||
uni.showToast({ title: '录音时间太短', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
// 发送语音消息
|
||||
emit('send', {
|
||||
tempFilePath: tempFilePath.value,
|
||||
tempFilePath: res.tempFilePath,
|
||||
duration: duration
|
||||
})
|
||||
})
|
||||
|
||||
// 录音错误
|
||||
recorderManager.value.onError((err) => {
|
||||
console.error('[VoiceRecorder] 录音错误:', err)
|
||||
clearInterval(recordingTimer.value)
|
||||
clearTimer()
|
||||
isRecording.value = false
|
||||
showModal.value = false
|
||||
isCancelling.value = false
|
||||
cancelled = false
|
||||
|
||||
uni.showToast({
|
||||
title: '录音失败',
|
||||
icon: 'none'
|
||||
})
|
||||
uni.showToast({ title: '录音失败,请检查麦克风权限', icon: 'none' })
|
||||
})
|
||||
}
|
||||
|
||||
// 开始录音
|
||||
const handleTouchStart = () => {
|
||||
if (!recorderManager.value) {
|
||||
initRecorder()
|
||||
const clearTimer = () => {
|
||||
if (recordingTimer.value) {
|
||||
clearInterval(recordingTimer.value)
|
||||
recordingTimer.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const stopRecording = () => {
|
||||
if (recorderManager.value) {
|
||||
recorderManager.value.stop()
|
||||
}
|
||||
}
|
||||
|
||||
// 开始录音
|
||||
const handleTouchStart = (e) => {
|
||||
startY.value = e.touches[0].clientY
|
||||
isCancelling.value = false
|
||||
cancelled = false
|
||||
|
||||
initRecorder()
|
||||
|
||||
// 开始录音
|
||||
recorderManager.value.start({
|
||||
duration: 60000, // 最长60秒
|
||||
duration: 60000,
|
||||
sampleRate: 16000,
|
||||
numberOfChannels: 1,
|
||||
encodeBitRate: 48000,
|
||||
|
|
@ -112,121 +142,117 @@ const handleTouchStart = () => {
|
|||
})
|
||||
}
|
||||
|
||||
// 结束录音
|
||||
// 手指移动 - 上滑取消
|
||||
const handleTouchMove = (e) => {
|
||||
if (!isRecording.value) return
|
||||
|
||||
const moveY = e.touches[0].clientY
|
||||
const diff = startY.value - moveY
|
||||
|
||||
// 上滑超过80px进入取消状态
|
||||
isCancelling.value = diff > 80
|
||||
}
|
||||
|
||||
// 松开手指 - 发送或取消
|
||||
const handleTouchEnd = () => {
|
||||
if (!isRecording.value) return
|
||||
|
||||
// 停止录音
|
||||
recorderManager.value.stop()
|
||||
isRecording.value = false
|
||||
if (isCancelling.value) {
|
||||
// 取消录音
|
||||
cancelled = true
|
||||
stopRecording()
|
||||
} else {
|
||||
// 正常结束,发送
|
||||
stopRecording()
|
||||
}
|
||||
}
|
||||
|
||||
// 取消录音
|
||||
// 系统取消(如来电等)
|
||||
const handleTouchCancel = () => {
|
||||
if (!isRecording.value) return
|
||||
|
||||
clearInterval(recordingTimer.value)
|
||||
isRecording.value = false
|
||||
recordingTip.value = '录音已取消'
|
||||
|
||||
// 停止录音但不发送
|
||||
recorderManager.value.stop()
|
||||
cancelled = true
|
||||
stopRecording()
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
if (recordingTimer.value) {
|
||||
clearInterval(recordingTimer.value)
|
||||
}
|
||||
clearTimer()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.voice-recorder {
|
||||
.voice-btn {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
background-color: #fff;
|
||||
border: 1rpx solid #ddd;
|
||||
border-radius: 8rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
user-select: none;
|
||||
|
||||
&.recording {
|
||||
background-color: #ff6b6b;
|
||||
border-color: #ff6b6b;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.voice-btn {
|
||||
flex: 1;
|
||||
height: 72rpx;
|
||||
line-height: 72rpx;
|
||||
text-align: center;
|
||||
background: #f5f5f5;
|
||||
border-radius: 36rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
|
||||
.voice-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
|
||||
.voice-modal-content {
|
||||
width: 300rpx;
|
||||
height: 300rpx;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
border-radius: 24rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.voice-icon {
|
||||
position: relative;
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.voice-wave {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 4rpx solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
|
||||
&.active {
|
||||
animation: wave 1.5s ease-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 80rpx;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.voice-tip {
|
||||
font-size: 28rpx;
|
||||
color: #fff;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.voice-time {
|
||||
font-size: 48rpx;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
&.recording {
|
||||
background: #e0e0e0;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.voice-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.voice-modal-content {
|
||||
width: 300rpx;
|
||||
height: 300rpx;
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
border-radius: 24rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.cancel {
|
||||
background: rgba(200, 50, 50, 0.85);
|
||||
}
|
||||
}
|
||||
|
||||
.voice-icon {
|
||||
position: relative;
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.icon {
|
||||
font-size: 60rpx;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.voice-wave {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
border: 4rpx solid rgba(255, 255, 255, 0.3);
|
||||
|
||||
&.active {
|
||||
animation: wave 1.2s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -240,4 +266,16 @@ onUnmounted(() => {
|
|||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.voice-tip {
|
||||
color: #fff;
|
||||
font-size: 26rpx;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.voice-time {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 24rpx;
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ const ENV = {
|
|||
}
|
||||
|
||||
// 当前环境 - 开发时使用 development,打包时改为 production
|
||||
const CURRENT_ENV = 'development'
|
||||
const CURRENT_ENV = 'production'
|
||||
|
||||
// 导出配置
|
||||
export const config = {
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@
|
|||
scroll-y
|
||||
scroll-with-animation
|
||||
:scroll-into-view="scrollToId"
|
||||
:style="{ paddingTop: (statusBarHeight + 44) + 'px' }"
|
||||
:style="{ paddingTop: (statusBarHeight + 44) + 'px', paddingBottom: (140 + keyboardHeight) + 'px' }"
|
||||
@scrolltoupper="loadMoreMessages"
|
||||
>
|
||||
<!-- 用户信息卡片 -->
|
||||
|
|
@ -270,7 +270,7 @@
|
|||
</scroll-view>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<view class="bottom-action-bar">
|
||||
<view class="bottom-action-bar" :style="{ bottom: keyboardHeight + 'px' }">
|
||||
<!-- 三个操作按钮 - 键盘弹起时隐藏 -->
|
||||
<view class="action-buttons" v-show="!isInputFocused && inputMode === 'text'">
|
||||
<button class="action-btn" @click="handleExchangeWeChat">
|
||||
|
|
@ -297,8 +297,10 @@
|
|||
type="text"
|
||||
placeholder="请输入..."
|
||||
placeholder-style="color: #999;"
|
||||
:adjust-position="true"
|
||||
:adjust-position="false"
|
||||
:hold-keyboard="true"
|
||||
confirm-type="send"
|
||||
confirm-hold
|
||||
@focus="handleInputFocus"
|
||||
@blur="handleInputBlur"
|
||||
@confirm="handleSendMessage"
|
||||
|
|
@ -317,15 +319,15 @@
|
|||
@send="handleSendVoice"
|
||||
/>
|
||||
|
||||
<!-- 发送按钮 -->
|
||||
<button
|
||||
<!-- 发送按钮 - 使用view避免抢夺input焦点 -->
|
||||
<view
|
||||
v-if="inputMode === 'text'"
|
||||
class="send-btn"
|
||||
:class="{ active: inputText.trim() }"
|
||||
@click="handleSendMessage"
|
||||
>
|
||||
发送
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
|
@ -388,6 +390,9 @@ const hasMore = ref(true)
|
|||
const pageIndex = ref(1)
|
||||
const isInputFocused = ref(false)
|
||||
|
||||
// 键盘高度
|
||||
const keyboardHeight = ref(0)
|
||||
|
||||
// 交换状态 - 记录是否已成功交换过微信/照片
|
||||
const hasExchangedWeChat = ref(false)
|
||||
const hasExchangedPhoto = ref(false)
|
||||
|
|
@ -1045,10 +1050,7 @@ const handleInputFocus = () => {
|
|||
}
|
||||
|
||||
const handleInputBlur = () => {
|
||||
// 延迟隐藏,避免点击发送按钮时按钮消失
|
||||
setTimeout(() => {
|
||||
isInputFocused.value = false
|
||||
}, 100)
|
||||
isInputFocused.value = false
|
||||
}
|
||||
|
||||
const shouldShowTime = (message, index) => {
|
||||
|
|
@ -1100,6 +1102,14 @@ const handleMore = () => {
|
|||
onMounted(async () => {
|
||||
getSystemInfo()
|
||||
|
||||
// 监听键盘高度变化
|
||||
uni.onKeyboardHeightChange((res) => {
|
||||
keyboardHeight.value = res.height || 0
|
||||
if (res.height > 0) {
|
||||
nextTick(() => scrollToBottom())
|
||||
}
|
||||
})
|
||||
|
||||
const pages = getCurrentPages()
|
||||
const currentPage = pages[pages.length - 1]
|
||||
const options = currentPage?.options || {}
|
||||
|
|
@ -1373,6 +1383,11 @@ const handleMessagesRead = (data) => {
|
|||
onUnmounted(() => {
|
||||
chatStore.clearCurrentSession()
|
||||
|
||||
// 移除键盘高度监听
|
||||
// #ifdef MP-WEIXIN
|
||||
uni.offKeyboardHeightChange()
|
||||
// #endif
|
||||
|
||||
// 停止语音播放
|
||||
stopVoice()
|
||||
if (innerAudioContext.value) {
|
||||
|
|
@ -1455,7 +1470,6 @@ onUnmounted(() => {
|
|||
.content-scroll {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
padding-bottom: 280rpx;
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
|
|
@ -1877,6 +1891,8 @@ onUnmounted(() => {
|
|||
right: 0;
|
||||
background: #f5f6fa;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
transition: bottom 0.15s ease-out;
|
||||
z-index: 100;
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
|
|
@ -2004,9 +2020,10 @@ onUnmounted(() => {
|
|||
color: #fff;
|
||||
border: none;
|
||||
flex-shrink: 0;
|
||||
text-align: center;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
&:active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&.active {
|
||||
|
|
|
|||
|
|
@ -66,12 +66,8 @@
|
|||
|
||||
<!-- 小程序订阅消息提醒条 - 固定在底部,会员广告关闭后显示 -->
|
||||
<view class="subscribe-reminder-section" v-if="showSubscribeReminder && !showMemberAd">
|
||||
<view class="subscribe-reminder-bar" :style="subscribeReminderBgStyle">
|
||||
<view class="reminder-content">
|
||||
<text class="reminder-text">开启消息通知,不错过任何心动对象</text>
|
||||
</view>
|
||||
<view class="reminder-btn" @click="handleOpenSubscribe">打开</view>
|
||||
<view class="reminder-close" @click.stop="handleCloseSubscribeReminder">
|
||||
<view class="member-ad-bar" :style="subscribeReminderBgStyle" @click="handleOpenSubscribe">
|
||||
<view class="ad-close" @click.stop="handleCloseSubscribeReminder">
|
||||
<text>×</text>
|
||||
</view>
|
||||
</view>
|
||||
|
|
@ -1304,53 +1300,5 @@ export default {
|
|||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 98;
|
||||
|
||||
.subscribe-reminder-bar {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 100rpx;
|
||||
padding: 0 24rpx;
|
||||
|
||||
.reminder-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.reminder-text {
|
||||
font-size: 26rpx;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.reminder-btn {
|
||||
background: #fff;
|
||||
color: #667eea;
|
||||
font-size: 24rpx;
|
||||
padding: 12rpx 32rpx;
|
||||
border-radius: 30rpx;
|
||||
margin-right: 50rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.reminder-close {
|
||||
position: absolute;
|
||||
right: 16rpx;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
text {
|
||||
font-size: 36rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -86,9 +86,9 @@ public class AdminUserController : ControllerBase
|
|||
[OperationLog("用户管理", "新增", Description = "创建测试用户")]
|
||||
public async Task<ApiResponse<List<long>>> CreateTestUsers([FromBody] CreateTestUsersRequest request)
|
||||
{
|
||||
if (request.Count <= 0 || request.Count > 50)
|
||||
if (request.Count <= 0 || request.Count > 100)
|
||||
{
|
||||
return ApiResponse<List<long>>.Error(40001, "创建数量必须在1-50之间");
|
||||
return ApiResponse<List<long>>.Error(40001, "创建数量必须在1-100之间");
|
||||
}
|
||||
|
||||
var result = await _adminUserService.CreateTestUsersAsync(request.Count, request.Gender);
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ public class OrderController : ControllerBase
|
|||
return ApiResponse<OrderCreateResponse>.Error(ErrorCodes.InvalidParameter, "订单类型无效");
|
||||
}
|
||||
|
||||
if (request.OrderType == 1 && (!request.MemberLevel.HasValue || request.MemberLevel < 1 || request.MemberLevel > 3))
|
||||
if (request.OrderType == 1 && (!request.MemberLevel.HasValue || request.MemberLevel < 1 || request.MemberLevel > 4))
|
||||
{
|
||||
return ApiResponse<OrderCreateResponse>.Error(ErrorCodes.InvalidParameter, "会员等级无效");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ public class PayController : ControllerBase
|
|||
[HttpPost("createOrder")]
|
||||
public async Task<ApiResponse<CreateOrderResponse>> CreateOrder([FromBody] CreateMemberOrderRequest request)
|
||||
{
|
||||
if (request.MemberLevel < 1 || request.MemberLevel > 3)
|
||||
if (request.MemberLevel < 1 || request.MemberLevel > 4)
|
||||
{
|
||||
return ApiResponse<CreateOrderResponse>.Error(ErrorCodes.InvalidParameter, "无效的会员等级");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ public class AdminUserService : IAdminUserService
|
|||
private readonly IRepository<UserPhoto> _photoRepository;
|
||||
private readonly IRepository<UserRequirement> _requirementRepository;
|
||||
private readonly IRepository<Order> _orderRepository;
|
||||
private readonly ISystemConfigService _systemConfigService;
|
||||
private readonly ILogger<AdminUserService> _logger;
|
||||
|
||||
public AdminUserService(
|
||||
|
|
@ -27,6 +28,7 @@ public class AdminUserService : IAdminUserService
|
|||
IRepository<UserPhoto> photoRepository,
|
||||
IRepository<UserRequirement> requirementRepository,
|
||||
IRepository<Order> orderRepository,
|
||||
ISystemConfigService systemConfigService,
|
||||
ILogger<AdminUserService> logger)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
|
|
@ -34,6 +36,7 @@ public class AdminUserService : IAdminUserService
|
|||
_photoRepository = photoRepository;
|
||||
_requirementRepository = requirementRepository;
|
||||
_orderRepository = orderRepository;
|
||||
_systemConfigService = systemConfigService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
|
|
@ -342,11 +345,8 @@ public class AdminUserService : IAdminUserService
|
|||
var parentHousingStatuses = new[] { "自有住房", "自有住房(已还清贷款)", "租房", "与子女同住" };
|
||||
var pensionStatuses = new[] { "城镇职工养老保险", "城乡居民养老保险", "无养老保险" };
|
||||
var medicalStatuses = new[] { "城镇职工医保", "城乡居民医保", "商业医疗保险" };
|
||||
// 默认头像列表
|
||||
var defaultAvatars = new[] {
|
||||
"/uploads/avatars/default_male.png",
|
||||
"/uploads/avatars/default_female.png"
|
||||
};
|
||||
// 从系统配置获取默认头像
|
||||
var defaultAvatar = await _systemConfigService.GetDefaultAvatarAsync() ?? "/uploads/avatars/default.png";
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
|
|
@ -381,7 +381,7 @@ public class AdminUserService : IAdminUserService
|
|||
{
|
||||
OpenId = $"test_openid_{Guid.NewGuid():N}",
|
||||
Nickname = nickname,
|
||||
Avatar = defaultAvatars[userGender == 1 ? 0 : 1],
|
||||
Avatar = defaultAvatar,
|
||||
Phone = phone,
|
||||
XiangQinNo = xiangQinNo,
|
||||
Gender = userGender,
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ public class OrderService : IOrderService
|
|||
if (request.OrderType == (int)OrderType.Membership)
|
||||
{
|
||||
// 会员订单 - 从数据库读取价格配置
|
||||
if (!request.MemberLevel.HasValue || request.MemberLevel < 1 || request.MemberLevel > 3)
|
||||
if (!request.MemberLevel.HasValue || request.MemberLevel < 1 || request.MemberLevel > 4)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.InvalidParameter, "无效的会员等级");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ public class PaymentService : IPaymentService
|
|||
Amount = tierConfig.Price,
|
||||
PayAmount = tierConfig.Price,
|
||||
Status = 1, // 待支付
|
||||
Remark = $"会员等级:{request.MemberLevel}",
|
||||
ExpireTime = DateTime.Now.AddMinutes(30), // 30分钟过期
|
||||
CreateTime = DateTime.Now,
|
||||
UpdateTime = DateTime.Now
|
||||
|
|
@ -167,10 +168,31 @@ public class PaymentService : IPaymentService
|
|||
/// </summary>
|
||||
private async Task ActivateMembershipAsync(Order order)
|
||||
{
|
||||
// 获取会员等级配置
|
||||
var tierConfigs = await _tierConfigRepository.GetListAsync(t => t.Name == order.ProductName);
|
||||
var tierConfig = tierConfigs.FirstOrDefault();
|
||||
var memberLevel = tierConfig?.Level ?? 1;
|
||||
// 从订单备注解析会员等级(格式:"会员等级:3")
|
||||
int? parsedLevel = null;
|
||||
if (!string.IsNullOrEmpty(order.Remark) && order.Remark.StartsWith("会员等级:"))
|
||||
{
|
||||
var levelStr = order.Remark.Replace("会员等级:", "");
|
||||
if (int.TryParse(levelStr, out var lv))
|
||||
{
|
||||
parsedLevel = lv;
|
||||
}
|
||||
}
|
||||
|
||||
// 优先用解析出的等级查配置,fallback 用商品名称匹配
|
||||
MemberTierConfig? tierConfig = null;
|
||||
if (parsedLevel.HasValue)
|
||||
{
|
||||
var tierConfigs = await _tierConfigRepository.GetListAsync(t => t.Level == parsedLevel.Value);
|
||||
tierConfig = tierConfigs.FirstOrDefault();
|
||||
}
|
||||
if (tierConfig == null)
|
||||
{
|
||||
var tierConfigs = await _tierConfigRepository.GetListAsync(t => t.Name == order.ProductName);
|
||||
tierConfig = tierConfigs.FirstOrDefault();
|
||||
}
|
||||
|
||||
var memberLevel = tierConfig?.Level ?? parsedLevel ?? 1;
|
||||
|
||||
// 检查是否已有会员记录
|
||||
var existingMembers = await _memberRepository.GetListAsync(m => m.UserId == order.UserId && m.Status == 1);
|
||||
|
|
|
|||
|
|
@ -278,8 +278,11 @@ public class RecommendService : IRecommendService
|
|||
// 确定推荐数量
|
||||
var recommendCount = GetRecommendCountByMemberLevel(user.MemberLevel);
|
||||
|
||||
// 多获取一些候选用户,弥补查询时可能被过滤掉的(用户被禁用、资料未审核等)
|
||||
var candidateCount = (int)Math.Ceiling(recommendCount * 1.5);
|
||||
|
||||
// 获取候选用户
|
||||
var candidates = await GetCandidateUsersAsync(userId, userProfile, userRequirement, recommendCount);
|
||||
var candidates = await GetCandidateUsersAsync(userId, userProfile, userRequirement, candidateCount);
|
||||
|
||||
// 删除今日已有的推荐
|
||||
var today = DateTime.Today;
|
||||
|
|
@ -349,6 +352,7 @@ public class RecommendService : IRecommendService
|
|||
1 => UnlimitedMemberRecommendCount, // 不限时会员:24人
|
||||
2 => _random.Next(SincereMemberMinRecommendCount, SincereMemberMaxRecommendCount + 1), // 诚意会员:24-29人
|
||||
3 => FamilyMemberRecommendCount, // 家庭版会员:24人
|
||||
4 => UnlimitedMemberRecommendCount, // 限时会员:24人
|
||||
_ => NormalUserRecommendCount
|
||||
};
|
||||
}
|
||||
|
|
@ -364,6 +368,7 @@ public class RecommendService : IRecommendService
|
|||
1 => (UnlimitedMemberRecommendCount, UnlimitedMemberRecommendCount),
|
||||
2 => (SincereMemberMinRecommendCount, SincereMemberMaxRecommendCount),
|
||||
3 => (FamilyMemberRecommendCount, FamilyMemberRecommendCount),
|
||||
4 => (UnlimitedMemberRecommendCount, UnlimitedMemberRecommendCount),
|
||||
_ => (NormalUserRecommendCount, NormalUserRecommendCount)
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user