|
@@ -32,8 +32,19 @@
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
|
<!-- 消息列表 -->
|
|
<!-- 消息列表 -->
|
|
|
- <scroll-view class="message-list" scroll-y :scroll-top="scrollTop" :scroll-into-view="scrollIntoView"
|
|
|
|
|
- @touchstart="handleListTouchStart" @touchmove="handleListTouchMove" @touchend="handleListTouchEnd">
|
|
|
|
|
|
|
+ <scroll-view
|
|
|
|
|
+ class="message-list"
|
|
|
|
|
+ scroll-y
|
|
|
|
|
+ :scroll-anchoring="true"
|
|
|
|
|
+ :scroll-into-view="scrollIntoView"
|
|
|
|
|
+ :upper-threshold="180"
|
|
|
|
|
+ @scroll="handleMessageListScroll"
|
|
|
|
|
+ @scrolltoupper="handleHistoryReachTop"
|
|
|
|
|
+ @touchstart="handleListTouchStart"
|
|
|
|
|
+ @touchmove="handleListTouchMove"
|
|
|
|
|
+ @touchend="handleListTouchEnd"
|
|
|
|
|
+ >
|
|
|
|
|
+ <view v-if="loadingMoreHistory" class="history-loading-tip">正在加载更多...</view>
|
|
|
<view class="message-item" :class="message.direction" v-for="(message, index) in messages" :key="index"
|
|
<view class="message-item" :class="message.direction" v-for="(message, index) in messages" :key="index"
|
|
|
:id="'msg-' + index">
|
|
:id="'msg-' + index">
|
|
|
<!-- 头像+时间区域 -->
|
|
<!-- 头像+时间区域 -->
|
|
@@ -42,6 +53,7 @@
|
|
|
class="avatar"
|
|
class="avatar"
|
|
|
:class="getAvatarClass(message)"
|
|
:class="getAvatarClass(message)"
|
|
|
:src="getAvatarSrc(message)"
|
|
:src="getAvatarSrc(message)"
|
|
|
|
|
+ @error="handleAvatarLoadError(message, index)"
|
|
|
mode="aspectFill"
|
|
mode="aspectFill"
|
|
|
></image>
|
|
></image>
|
|
|
<text class="msg-time">{{ message.time || '--:--' }}</text>
|
|
<text class="msg-time">{{ message.time || '--:--' }}</text>
|
|
@@ -155,6 +167,7 @@
|
|
|
<view
|
|
<view
|
|
|
v-if="inputMode === 'voice'"
|
|
v-if="inputMode === 'voice'"
|
|
|
class="press-to-talk"
|
|
class="press-to-talk"
|
|
|
|
|
+ @touchstart="handlePressToTalkTouchStart"
|
|
|
@longpress="handlePressToTalkStart"
|
|
@longpress="handlePressToTalkStart"
|
|
|
@touchmove.stop.prevent="handlePressToTalkMove"
|
|
@touchmove.stop.prevent="handlePressToTalkMove"
|
|
|
@touchend.stop.prevent="handlePressToTalkEnd"
|
|
@touchend.stop.prevent="handlePressToTalkEnd"
|
|
@@ -174,6 +187,7 @@
|
|
|
@confirm="sendTextMessage"
|
|
@confirm="sendTextMessage"
|
|
|
maxlength="-1"
|
|
maxlength="-1"
|
|
|
@linechange="handleTextLineChange"
|
|
@linechange="handleTextLineChange"
|
|
|
|
|
+ @focus="handleTextInputFocus"
|
|
|
@blur="inputFocus = false"
|
|
@blur="inputFocus = false"
|
|
|
/>
|
|
/>
|
|
|
</view>
|
|
</view>
|
|
@@ -191,12 +205,12 @@
|
|
|
<view class="bottom-panel" :class="{ open: showMorePanel || showEmojiPanel }" @click.stop>
|
|
<view class="bottom-panel" :class="{ open: showMorePanel || showEmojiPanel }" @click.stop>
|
|
|
<view v-show="showMorePanel" class="more-panel">
|
|
<view v-show="showMorePanel" class="more-panel">
|
|
|
<view class="more-grid">
|
|
<view class="more-grid">
|
|
|
- <view class="more-item" @click="handleMoreAction('file')">
|
|
|
|
|
|
|
+ <!-- <view class="more-item" @click="handleMoreAction('file')">
|
|
|
<view class="more-icon">
|
|
<view class="more-icon">
|
|
|
<Icon lib="base" name="icon-file" size="36" :color="iconColor" />
|
|
<Icon lib="base" name="icon-file" size="36" :color="iconColor" />
|
|
|
</view>
|
|
</view>
|
|
|
<text class="more-text">文件</text>
|
|
<text class="more-text">文件</text>
|
|
|
- </view>
|
|
|
|
|
|
|
+ </view> -->
|
|
|
<view class="more-item" @click="handleMoreAction('image')">
|
|
<view class="more-item" @click="handleMoreAction('image')">
|
|
|
<view class="more-icon">
|
|
<view class="more-icon">
|
|
|
<Icon lib="base" name="icon-img" size="36" :color="iconColor" />
|
|
<Icon lib="base" name="icon-img" size="36" :color="iconColor" />
|
|
@@ -286,12 +300,25 @@
|
|
|
@confirm="handleModalConfirm"
|
|
@confirm="handleModalConfirm"
|
|
|
@cancel="handleModalCancel"
|
|
@cancel="handleModalCancel"
|
|
|
/>
|
|
/>
|
|
|
|
|
+
|
|
|
|
|
+ <SsConfirm
|
|
|
|
|
+ :visible="serviceConfirmVisible"
|
|
|
|
|
+ title="提示"
|
|
|
|
|
+ :showHeader="true"
|
|
|
|
|
+ width="560rpx"
|
|
|
|
|
+ height="420rpx"
|
|
|
|
|
+ :bottom-buttons="serviceConfirmButtons"
|
|
|
|
|
+ @button-click="handleServiceConfirmAction"
|
|
|
|
|
+ >
|
|
|
|
|
+ <view class="service-confirm-body">{{ serviceConfirmContent || '当前服务不可用' }}</view>
|
|
|
|
|
+ </SsConfirm>
|
|
|
</view>
|
|
</view>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script>
|
|
<script>
|
|
|
import Icon from '@/components/icon/index.vue'
|
|
import Icon from '@/components/icon/index.vue'
|
|
|
import customModal from '@/components/custom-modal.vue'
|
|
import customModal from '@/components/custom-modal.vue'
|
|
|
|
|
+import SsConfirm from '@/components/SsConfirm/index.vue'
|
|
|
import { EMOJI_LIST } from '@/constants/emoji'
|
|
import { EMOJI_LIST } from '@/constants/emoji'
|
|
|
import { collectImageUrls, createImageMessage, createTextMessage } from '@/utils/parent-message-factory'
|
|
import { collectImageUrls, createImageMessage, createTextMessage } from '@/utils/parent-message-factory'
|
|
|
import {
|
|
import {
|
|
@@ -314,6 +341,7 @@ const REC_RYLBM_STUDENT = 1100
|
|
|
const REC_RYLBM_PARENT = 1200
|
|
const REC_RYLBM_PARENT = 1200
|
|
|
const CALL_END_STORAGE_KEY = 'parentLastCallInfo'
|
|
const CALL_END_STORAGE_KEY = 'parentLastCallInfo'
|
|
|
const CALL_END_PAGE_URL = '/pages/parent/message'
|
|
const CALL_END_PAGE_URL = '/pages/parent/message'
|
|
|
|
|
+const HISTORY_MORE_CMD = 162 // 分页加载旧消息命令
|
|
|
|
|
|
|
|
let wmpfVoip = null
|
|
let wmpfVoip = null
|
|
|
try {
|
|
try {
|
|
@@ -355,10 +383,11 @@ const miniprogramState = (() => {
|
|
|
default: ''
|
|
default: ''
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
- components: {
|
|
|
|
|
- Icon,
|
|
|
|
|
- customModal
|
|
|
|
|
- },
|
|
|
|
|
|
|
+ components: {
|
|
|
|
|
+ Icon,
|
|
|
|
|
+ customModal,
|
|
|
|
|
+ SsConfirm
|
|
|
|
|
+ },
|
|
|
computed: {},
|
|
computed: {},
|
|
|
watch: {
|
|
watch: {
|
|
|
role() {
|
|
role() {
|
|
@@ -379,8 +408,7 @@ const miniprogramState = (() => {
|
|
|
},
|
|
},
|
|
|
data() {
|
|
data() {
|
|
|
return {
|
|
return {
|
|
|
- scrollTop: 0,
|
|
|
|
|
- scrollIntoView: '',
|
|
|
|
|
|
|
+ scrollIntoView: '',
|
|
|
inputMode: 'voice', // voice | text
|
|
inputMode: 'voice', // voice | text
|
|
|
inputFocus: false,
|
|
inputFocus: false,
|
|
|
draftText: '',
|
|
draftText: '',
|
|
@@ -405,6 +433,14 @@ const miniprogramState = (() => {
|
|
|
historyLoading: false,
|
|
historyLoading: false,
|
|
|
historyWaiter: null,
|
|
historyWaiter: null,
|
|
|
historyWaiterTimer: null,
|
|
historyWaiterTimer: null,
|
|
|
|
|
+ historyMode: 'initial',
|
|
|
|
|
+ historyBatchCount: 0,
|
|
|
|
|
+ historyPageNo: 0,
|
|
|
|
|
+ historyPendingMessages: [],
|
|
|
|
|
+ loadingMoreHistory: false,
|
|
|
|
|
+ hasMoreHistory: true,
|
|
|
|
|
+ historyCursorMsgId: '',
|
|
|
|
|
+ historyTopLoadArmed: true,
|
|
|
parentInfo: {
|
|
parentInfo: {
|
|
|
xm: '',
|
|
xm: '',
|
|
|
openid: ''
|
|
openid: ''
|
|
@@ -425,11 +461,14 @@ const miniprogramState = (() => {
|
|
|
isRecording: false,
|
|
isRecording: false,
|
|
|
isRecordCancel: false,
|
|
isRecordCancel: false,
|
|
|
recordSeconds: 0,
|
|
recordSeconds: 0,
|
|
|
- recordStartY: 0,
|
|
|
|
|
- recordTickTimer: null,
|
|
|
|
|
- recordGuardTimer: null,
|
|
|
|
|
- audioPlayer: null,
|
|
|
|
|
- playingVoiceIndex: -1,
|
|
|
|
|
|
|
+ recordStartY: 0,
|
|
|
|
|
+ recordTickTimer: null,
|
|
|
|
|
+ recordGuardTimer: null,
|
|
|
|
|
+ recordPermissionPromise: null,
|
|
|
|
|
+ isPressingToTalk: false,
|
|
|
|
|
+ selfAvatarLoadFailed: false,
|
|
|
|
|
+ audioPlayer: null,
|
|
|
|
|
+ playingVoiceIndex: -1,
|
|
|
sessionInitialized: false,
|
|
sessionInitialized: false,
|
|
|
sessionRole: 'parent',
|
|
sessionRole: 'parent',
|
|
|
sessionContactId: '',
|
|
sessionContactId: '',
|
|
@@ -456,6 +495,9 @@ const miniprogramState = (() => {
|
|
|
modalShowCancel: false,
|
|
modalShowCancel: false,
|
|
|
modalConfirmText: '确定',
|
|
modalConfirmText: '确定',
|
|
|
modalResolve: null,
|
|
modalResolve: null,
|
|
|
|
|
+ serviceConfirmVisible: false,
|
|
|
|
|
+ serviceConfirmContent: '',
|
|
|
|
|
+ serviceConfirmButtons: [{ text: '关闭' }],
|
|
|
loadOptions: {},
|
|
loadOptions: {},
|
|
|
// 消息列表数据
|
|
// 消息列表数据
|
|
|
messages: []
|
|
messages: []
|
|
@@ -491,10 +533,9 @@ const miniprogramState = (() => {
|
|
|
if (this.sessionRole === 'device') {
|
|
if (this.sessionRole === 'device') {
|
|
|
await (this.deviceInitPromise || Promise.resolve(true))
|
|
await (this.deviceInitPromise || Promise.resolve(true))
|
|
|
if (!this.deviceSessionReady) return
|
|
if (!this.deviceSessionReady) return
|
|
|
- await this.checkServiceStatus(115)
|
|
|
|
|
- await this.checkServiceStatus(111)
|
|
|
|
|
this.resetInactivityTimer()
|
|
this.resetInactivityTimer()
|
|
|
}
|
|
}
|
|
|
|
|
+ await this.checkServiceStatus()
|
|
|
await this.bootstrapMessageFlow()
|
|
await this.bootstrapMessageFlow()
|
|
|
},
|
|
},
|
|
|
onUnload() {
|
|
onUnload() {
|
|
@@ -797,30 +838,32 @@ const miniprogramState = (() => {
|
|
|
},
|
|
},
|
|
|
getSelfAvatarMeta() {
|
|
getSelfAvatarMeta() {
|
|
|
const userInfo = this.getStoredUserInfo()
|
|
const userInfo = this.getStoredUserInfo()
|
|
|
|
|
+ const studentAvatar = this.normalizeMediaPath(userInfo.zjzwj)
|
|
|
|
|
+ const parentAvatar = this.normalizeMediaPath(userInfo.yszwj)
|
|
|
const peerType = String(this.peerAvatarTypeHint || '')
|
|
const peerType = String(this.peerAvatarTypeHint || '')
|
|
|
const preferType = peerType === '1' ? '51' : (peerType === '51' ? '1' : '')
|
|
const preferType = peerType === '1' ? '51' : (peerType === '51' ? '1' : '')
|
|
|
if (preferType === '51') {
|
|
if (preferType === '51') {
|
|
|
return {
|
|
return {
|
|
|
- url: this.toDisplayImageUrl(userInfo.zjzwj || userInfo.yszwj || ''),
|
|
|
|
|
|
|
+ url: this.toDisplayImageUrl(studentAvatar || parentAvatar || ''),
|
|
|
type: '51'
|
|
type: '51'
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
if (preferType === '1') {
|
|
if (preferType === '1') {
|
|
|
return {
|
|
return {
|
|
|
- url: this.toDisplayImageUrl(userInfo.yszwj || userInfo.zjzwj || ''),
|
|
|
|
|
|
|
+ url: this.toDisplayImageUrl(parentAvatar || studentAvatar || ''),
|
|
|
type: '1'
|
|
type: '1'
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
if (this.sessionRole === 'device') {
|
|
if (this.sessionRole === 'device') {
|
|
|
- if (userInfo.zjzwj) {
|
|
|
|
|
|
|
+ if (studentAvatar) {
|
|
|
return {
|
|
return {
|
|
|
- url: this.toDisplayImageUrl(userInfo.zjzwj),
|
|
|
|
|
|
|
+ url: this.toDisplayImageUrl(studentAvatar),
|
|
|
type: '51'
|
|
type: '51'
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- if (userInfo.yszwj) {
|
|
|
|
|
|
|
+ if (parentAvatar) {
|
|
|
return {
|
|
return {
|
|
|
- url: this.toDisplayImageUrl(userInfo.yszwj),
|
|
|
|
|
|
|
+ url: this.toDisplayImageUrl(parentAvatar),
|
|
|
type: '1'
|
|
type: '1'
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -829,15 +872,15 @@ const miniprogramState = (() => {
|
|
|
type: ''
|
|
type: ''
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- if (userInfo.yszwj) {
|
|
|
|
|
|
|
+ if (parentAvatar) {
|
|
|
return {
|
|
return {
|
|
|
- url: this.toDisplayImageUrl(userInfo.yszwj),
|
|
|
|
|
|
|
+ url: this.toDisplayImageUrl(parentAvatar),
|
|
|
type: '1'
|
|
type: '1'
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- if (userInfo.zjzwj) {
|
|
|
|
|
|
|
+ if (studentAvatar) {
|
|
|
return {
|
|
return {
|
|
|
- url: this.toDisplayImageUrl(userInfo.zjzwj),
|
|
|
|
|
|
|
+ url: this.toDisplayImageUrl(studentAvatar),
|
|
|
type: '51'
|
|
type: '51'
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -848,9 +891,22 @@ const miniprogramState = (() => {
|
|
|
},
|
|
},
|
|
|
getAvatarSrc(message = {}) {
|
|
getAvatarSrc(message = {}) {
|
|
|
if (!message || message.direction === 'right') {
|
|
if (!message || message.direction === 'right') {
|
|
|
|
|
+ if (this.selfAvatarLoadFailed) return '/static/logo.png'
|
|
|
return this.getSelfAvatarMeta().url
|
|
return this.getSelfAvatarMeta().url
|
|
|
}
|
|
}
|
|
|
- return message.avatarUrl || '/static/logo.png'
|
|
|
|
|
|
|
+ const peerAvatar = this.normalizeMediaPath(message.avatarUrl)
|
|
|
|
|
+ return peerAvatar || '/static/logo.png'
|
|
|
|
|
+ },
|
|
|
|
|
+ handleAvatarLoadError(message, index) {
|
|
|
|
|
+ const failedUrl = this.getAvatarSrc(message)
|
|
|
|
|
+ console.warn('[Avatar] load failed:', failedUrl, 'direction:', message?.direction, 'index:', index)
|
|
|
|
|
+ if (!message || message.direction === 'right') {
|
|
|
|
|
+ this.selfAvatarLoadFailed = true
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ if (typeof index === 'number' && this.messages[index]) {
|
|
|
|
|
+ this.$set(this.messages[index], 'avatarUrl', '')
|
|
|
|
|
+ }
|
|
|
},
|
|
},
|
|
|
getAvatarClass(message = {}) {
|
|
getAvatarClass(message = {}) {
|
|
|
const type = String(
|
|
const type = String(
|
|
@@ -876,22 +932,24 @@ const miniprogramState = (() => {
|
|
|
if (this.sessionRole === 'device') {
|
|
if (this.sessionRole === 'device') {
|
|
|
return {
|
|
return {
|
|
|
username: member.xm || member.username || '家长',
|
|
username: member.xm || member.username || '家长',
|
|
|
- avatar: member.yszwj ? this.toDisplayImageUrl(member.yszwj) : '/static/logo.png',
|
|
|
|
|
|
|
+ avatar: this.toDisplayImageUrl(member.yszwj),
|
|
|
openid: member.wbid2 || member.wbid || ''
|
|
openid: member.wbid2 || member.wbid || ''
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
return {
|
|
return {
|
|
|
username: member.xm || member.username || '学生',
|
|
username: member.xm || member.username || '学生',
|
|
|
- avatar: member.yszwj ? this.toDisplayImageUrl(member.yszwj) : '/static/logo.png',
|
|
|
|
|
|
|
+ avatar: this.toDisplayImageUrl(member.yszwj),
|
|
|
deviceSn: member.deviceSn || member.sn || member.devId || member.sbkh || '',
|
|
deviceSn: member.deviceSn || member.sn || member.devId || member.sbkh || '',
|
|
|
pushToken: member.pushToken || member.voipToken || member.token || ''
|
|
pushToken: member.pushToken || member.voipToken || member.token || ''
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
async startVoiceCall() {
|
|
async startVoiceCall() {
|
|
|
|
|
+ if (!this.ensureFeatureAvailable(111)) return
|
|
|
const contact = this.buildCallContactByCurrentMember()
|
|
const contact = this.buildCallContactByCurrentMember()
|
|
|
await this.startCall(contact, 'voice')
|
|
await this.startCall(contact, 'voice')
|
|
|
},
|
|
},
|
|
|
async startVideoCall() {
|
|
async startVideoCall() {
|
|
|
|
|
+ if (!this.ensureFeatureAvailable(115)) return
|
|
|
const contact = this.buildCallContactByCurrentMember()
|
|
const contact = this.buildCallContactByCurrentMember()
|
|
|
await this.startCall(contact, 'video')
|
|
await this.startCall(contact, 'video')
|
|
|
},
|
|
},
|
|
@@ -1047,23 +1105,78 @@ const miniprogramState = (() => {
|
|
|
this.handleDeviceLogout()
|
|
this.handleDeviceLogout()
|
|
|
}, this.inactivityTimeout)
|
|
}, this.inactivityTimeout)
|
|
|
},
|
|
},
|
|
|
- async checkServiceStatus(grfwxmm) {
|
|
|
|
|
- if (this.sessionRole !== 'device') return
|
|
|
|
|
- const code = Number(grfwxmm)
|
|
|
|
|
- if (code !== 111 && code !== 115) {
|
|
|
|
|
- console.warn('checkServiceStatus skip: invalid grfwxmm', grfwxmm)
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ async checkServiceStatus() {
|
|
|
try {
|
|
try {
|
|
|
- const result = await deviceApi.grfw_chkGrfw(code)
|
|
|
|
|
|
|
+ const result = this.sessionRole === 'device'
|
|
|
|
|
+ ? await deviceApi.grfw_chkGrfw()
|
|
|
|
|
+ : await grfwApi.grfw_chkGrfw()
|
|
|
const data = result?.data || {}
|
|
const data = result?.data || {}
|
|
|
- if (data.ssCode === 0 && data.ssData) {
|
|
|
|
|
- this.serviceStatus = data.ssData
|
|
|
|
|
|
|
+ console.log('[ServiceStatus] grfw_chkGrfw raw:', data)
|
|
|
|
|
+ if (data.ssCode === 0) {
|
|
|
|
|
+ const sourceList = Array.isArray(data.ssData) ? data.ssData : []
|
|
|
|
|
+ const statusMap = {}
|
|
|
|
|
+ sourceList.forEach((item) => {
|
|
|
|
|
+ if (!item || typeof item !== 'object') return
|
|
|
|
|
+ Object.keys(item).forEach((serviceCode) => {
|
|
|
|
|
+ const serviceValue = item[serviceCode]
|
|
|
|
|
+ if (!serviceValue || typeof serviceValue !== 'object') return
|
|
|
|
|
+ const stateCode = Object.keys(serviceValue)[0] || ''
|
|
|
|
|
+ statusMap[String(serviceCode)] = {
|
|
|
|
|
+ stateCode: String(stateCode),
|
|
|
|
|
+ stateMsg: String(serviceValue[stateCode] || '')
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+ this.serviceStatus = statusMap
|
|
|
|
|
+ console.log('[ServiceStatus] parsed map:', this.serviceStatus)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.warn('[ServiceStatus] non-zero ssCode:', data.ssCode, data.ssMsg || '')
|
|
|
}
|
|
}
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('检查服务状态失败:', error)
|
|
console.error('检查服务状态失败:', error)
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
|
|
+ decodeStateMsg(raw = '') {
|
|
|
|
|
+ const text = String(raw || '').trim()
|
|
|
|
|
+ if (!text) return ''
|
|
|
|
|
+ if (/[\u4e00-\u9fa5]/.test(text)) return text
|
|
|
|
|
+ try {
|
|
|
|
|
+ return decodeURIComponent(escape(text))
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ return text
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ getFeatureLimitByCode(code) {
|
|
|
|
|
+ const key = String(code || '')
|
|
|
|
|
+ if (!key) return null
|
|
|
|
|
+ if (!this.serviceStatus || typeof this.serviceStatus !== 'object') return null
|
|
|
|
|
+ return this.serviceStatus[key] || null
|
|
|
|
|
+ },
|
|
|
|
|
+ ensureFeatureAvailable(code) {
|
|
|
|
|
+ // 临时放开:当前阶段所有功能都视为可用,不做余额/订阅拦截。
|
|
|
|
|
+ // 后续恢复时,取消下面注释并删除 return true 即可。
|
|
|
|
|
+ // const limitInfo = this.getFeatureLimitByCode(code)
|
|
|
|
|
+ // if (!limitInfo) return true
|
|
|
|
|
+ // const stateMsg = this.decodeStateMsg(limitInfo.stateMsg || '当前服务不可用')
|
|
|
|
|
+ // if (this.sessionRole === 'device') {
|
|
|
|
|
+ // this.serviceConfirmButtons = [{ text: '关闭' }]
|
|
|
|
|
+ // this.serviceConfirmContent = `${stateMsg},请让家长订阅后再尝试`
|
|
|
|
|
+ // } else {
|
|
|
|
|
+ // this.serviceConfirmButtons = [{ text: '关闭' }, { text: '确定' }]
|
|
|
|
|
+ // this.serviceConfirmContent = `${stateMsg},请订阅后再尝试`
|
|
|
|
|
+ // }
|
|
|
|
|
+ // this.serviceConfirmVisible = true
|
|
|
|
|
+ // return false
|
|
|
|
|
+ void code
|
|
|
|
|
+ return true
|
|
|
|
|
+ },
|
|
|
|
|
+ handleServiceConfirmAction(event) {
|
|
|
|
|
+ const index = Number(event?.index ?? -1)
|
|
|
|
|
+ this.serviceConfirmVisible = false
|
|
|
|
|
+ if (this.sessionRole === 'parent' && index === 1) {
|
|
|
|
|
+ uni.navigateTo({ url: '/pages/payment/recharge' })
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
async recordCallAndRefresh(callOptions = {}) {
|
|
async recordCallAndRefresh(callOptions = {}) {
|
|
|
if (this.sessionRole !== 'device') return
|
|
if (this.sessionRole !== 'device') return
|
|
|
try {
|
|
try {
|
|
@@ -1078,7 +1191,7 @@ const miniprogramState = (() => {
|
|
|
ll: 0,
|
|
ll: 0,
|
|
|
ms: `通话${duration}秒`
|
|
ms: `通话${duration}秒`
|
|
|
})
|
|
})
|
|
|
- await this.checkServiceStatus(grfwxmm)
|
|
|
|
|
|
|
+ await this.checkServiceStatus()
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('记录通话失败:', error)
|
|
console.error('记录通话失败:', error)
|
|
|
}
|
|
}
|
|
@@ -1234,6 +1347,7 @@ const miniprogramState = (() => {
|
|
|
},
|
|
},
|
|
|
async finishHistoryLoading() {
|
|
async finishHistoryLoading() {
|
|
|
this.historyLoading = false
|
|
this.historyLoading = false
|
|
|
|
|
+ this.loadingMoreHistory = false
|
|
|
if (this.historyWaiterTimer) {
|
|
if (this.historyWaiterTimer) {
|
|
|
clearTimeout(this.historyWaiterTimer)
|
|
clearTimeout(this.historyWaiterTimer)
|
|
|
this.historyWaiterTimer = null
|
|
this.historyWaiterTimer = null
|
|
@@ -1246,7 +1360,7 @@ const miniprogramState = (() => {
|
|
|
await websocketService.disconnect()
|
|
await websocketService.disconnect()
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
- async loadHistoryMessages({ reset = true } = {}) {
|
|
|
|
|
|
|
+ async loadHistoryMessages({ reset = true, cmd = 161, mode = 'initial' } = {}) {
|
|
|
if (this.historyLoading) return
|
|
if (this.historyLoading) return
|
|
|
if (!this.wsIdentity.sendRyid || !this.wsIdentity.recRyid) {
|
|
if (!this.wsIdentity.sendRyid || !this.wsIdentity.recRyid) {
|
|
|
console.warn('loadHistoryMessages skip: missing wsIdentity', this.wsIdentity)
|
|
console.warn('loadHistoryMessages skip: missing wsIdentity', this.wsIdentity)
|
|
@@ -1261,22 +1375,38 @@ const miniprogramState = (() => {
|
|
|
this.messages = []
|
|
this.messages = []
|
|
|
this.scrollIntoView = ''
|
|
this.scrollIntoView = ''
|
|
|
this.peerAvatarTypeHint = ''
|
|
this.peerAvatarTypeHint = ''
|
|
|
|
|
+ this.hasMoreHistory = true
|
|
|
|
|
+ this.historyCursorMsgId = ''
|
|
|
|
|
+ this.historyPageNo = 0
|
|
|
|
|
+ this.historyTopLoadArmed = true
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const config = this.currentWsConfig || this.buildWsConnectOptions()
|
|
const config = this.currentWsConfig || this.buildWsConnectOptions()
|
|
|
this.historyLoading = true
|
|
this.historyLoading = true
|
|
|
|
|
+ this.historyMode = mode
|
|
|
|
|
+ this.historyBatchCount = 0
|
|
|
|
|
+ this.historyPendingMessages = []
|
|
|
|
|
+ this.loadingMoreHistory = mode === 'older'
|
|
|
try {
|
|
try {
|
|
|
console.log('loadHistoryMessages ensureConnected start')
|
|
console.log('loadHistoryMessages ensureConnected start')
|
|
|
await websocketService.ensureConnected(config)
|
|
await websocketService.ensureConnected(config)
|
|
|
console.log('loadHistoryMessages ensureConnected done')
|
|
console.log('loadHistoryMessages ensureConnected done')
|
|
|
- await websocketService.send({
|
|
|
|
|
- cmd: 161,
|
|
|
|
|
|
|
+ const payload = {
|
|
|
|
|
+ cmd,
|
|
|
sendRyid: this.wsIdentity.sendRyid,
|
|
sendRyid: this.wsIdentity.sendRyid,
|
|
|
- recRyid: this.wsIdentity.recRyid
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ recRyid: this.wsIdentity.recRyid,
|
|
|
|
|
+ pageNo: mode === 'older' ? this.historyPageNo + 1 : 0
|
|
|
|
|
+ }
|
|
|
|
|
+ if (cmd === HISTORY_MORE_CMD && this.historyCursorMsgId) {
|
|
|
|
|
+ payload.beforeXxid = this.historyCursorMsgId
|
|
|
|
|
+ payload.lastXxid = this.historyCursorMsgId
|
|
|
|
|
+ }
|
|
|
|
|
+ console.log('[History] request payload:', payload)
|
|
|
|
|
+ await websocketService.send(payload)
|
|
|
await this.waitHistoryDone()
|
|
await this.waitHistoryDone()
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
this.historyLoading = false
|
|
this.historyLoading = false
|
|
|
|
|
+ this.loadingMoreHistory = false
|
|
|
console.error('拉取历史留言失败:', error)
|
|
console.error('拉取历史留言失败:', error)
|
|
|
if (this.sessionRole === 'parent') {
|
|
if (this.sessionRole === 'parent') {
|
|
|
await websocketService.disconnect()
|
|
await websocketService.disconnect()
|
|
@@ -1349,8 +1479,8 @@ const miniprogramState = (() => {
|
|
|
const un5 = websocketService.on('cmd:165', (payload) => {
|
|
const un5 = websocketService.on('cmd:165', (payload) => {
|
|
|
this.handleWsHistoryMessage(payload)
|
|
this.handleWsHistoryMessage(payload)
|
|
|
})
|
|
})
|
|
|
- const un6 = websocketService.on('cmd:11', () => {
|
|
|
|
|
- this.handleWsHistoryDone()
|
|
|
|
|
|
|
+ const un6 = websocketService.on('cmd:11', (payload) => {
|
|
|
|
|
+ this.handleWsHistoryDone(payload)
|
|
|
})
|
|
})
|
|
|
const un7 = websocketService.on('cmd:151', (payload) => {
|
|
const un7 = websocketService.on('cmd:151', (payload) => {
|
|
|
this.handleWsReceipt(payload)
|
|
this.handleWsReceipt(payload)
|
|
@@ -1392,10 +1522,18 @@ const miniprogramState = (() => {
|
|
|
console.error('设备端 WS 连接失败:', error)
|
|
console.error('设备端 WS 连接失败:', error)
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
|
|
+ normalizeMediaPath(path) {
|
|
|
|
|
+ const text = String(path || '').trim()
|
|
|
|
|
+ if (!text) return ''
|
|
|
|
|
+ const lowerText = text.toLowerCase()
|
|
|
|
|
+ if (lowerText === 'null' || lowerText === 'undefined') return ''
|
|
|
|
|
+ return text
|
|
|
|
|
+ },
|
|
|
toDisplayImageUrl(path) {
|
|
toDisplayImageUrl(path) {
|
|
|
- if (!path) return '/static/logo.png'
|
|
|
|
|
- if (/^https?:\/\//.test(path)) return path
|
|
|
|
|
- return getImageUrl(path)
|
|
|
|
|
|
|
+ const safePath = this.normalizeMediaPath(path)
|
|
|
|
|
+ if (!safePath) return '/static/logo.png'
|
|
|
|
|
+ if (/^https?:\/\//.test(safePath)) return safePath
|
|
|
|
|
+ return getImageUrl(safePath)
|
|
|
},
|
|
},
|
|
|
toDisplayFileUrl(path, type = '') {
|
|
toDisplayFileUrl(path, type = '') {
|
|
|
if (!path) return ''
|
|
if (!path) return ''
|
|
@@ -1424,8 +1562,8 @@ const miniprogramState = (() => {
|
|
|
}
|
|
}
|
|
|
return cont
|
|
return cont
|
|
|
},
|
|
},
|
|
|
- appendPayloadMessage(payload = {}, { fromHistory = false } = {}) {
|
|
|
|
|
- if (!this.shouldHandleIncomingPayload(payload)) return
|
|
|
|
|
|
|
+ appendPayloadMessage(payload = {}, { fromHistory = false, prepend = false, collectOnly = false } = {}) {
|
|
|
|
|
+ if (!this.shouldHandleIncomingPayload(payload)) return false
|
|
|
const cont = this.normalizeIncomingCont(payload.cont)
|
|
const cont = this.normalizeIncomingCont(payload.cont)
|
|
|
const typeCode = String(cont.type || '121')
|
|
const typeCode = String(cont.type || '121')
|
|
|
const direction = String(payload.sendRyid || '') === String(this.wsIdentity.sendRyid || '')
|
|
const direction = String(payload.sendRyid || '') === String(this.wsIdentity.sendRyid || '')
|
|
@@ -1441,16 +1579,43 @@ const miniprogramState = (() => {
|
|
|
const msgId = payload.xxid || payload.msgId || ''
|
|
const msgId = payload.xxid || payload.msgId || ''
|
|
|
if (fromHistory && msgId) {
|
|
if (fromHistory && msgId) {
|
|
|
const exists = this.messages.some((item) => String(item.msgId || '') === String(msgId))
|
|
const exists = this.messages.some((item) => String(item.msgId || '') === String(msgId))
|
|
|
- if (exists) return
|
|
|
|
|
|
|
+ if (exists) return false
|
|
|
|
|
+ if (collectOnly) {
|
|
|
|
|
+ const inPending = this.historyPendingMessages.some((item) => String(item.msgId || '') === String(msgId))
|
|
|
|
|
+ if (inPending) return false
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (fromHistory && msgId) {
|
|
|
|
|
+ const numMsgId = Number(msgId)
|
|
|
|
|
+ if (!Number.isNaN(numMsgId) && numMsgId > 0) {
|
|
|
|
|
+ if (!this.historyCursorMsgId || numMsgId < Number(this.historyCursorMsgId)) {
|
|
|
|
|
+ this.historyCursorMsgId = String(numMsgId)
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if (!this.historyCursorMsgId) {
|
|
|
|
|
+ this.historyCursorMsgId = String(msgId)
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
const payloadTime = this.formatPayloadTime(payload.sendTime || payload.sendTimeStr || payload.time)
|
|
const payloadTime = this.formatPayloadTime(payload.sendTime || payload.sendTimeStr || payload.time)
|
|
|
const displayTime = payloadTime || this.formatPayloadTime(new Date())
|
|
const displayTime = payloadTime || this.formatPayloadTime(new Date())
|
|
|
const receiptStatus = (payload.readTime || payload.rdTime || payload.readTm) ? 'read' : 'unread'
|
|
const receiptStatus = (payload.readTime || payload.rdTime || payload.readTm) ? 'read' : 'unread'
|
|
|
|
|
+ const pushMessage = (message) => {
|
|
|
|
|
+ if (!message) return false
|
|
|
|
|
+ if (collectOnly) {
|
|
|
|
|
+ this.historyPendingMessages.push(message)
|
|
|
|
|
+ return true
|
|
|
|
|
+ }
|
|
|
|
|
+ if (prepend) {
|
|
|
|
|
+ this.prependMessage(message)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.appendMessage(message)
|
|
|
|
|
+ }
|
|
|
|
|
+ return true
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
if (typeCode === '121') {
|
|
if (typeCode === '121') {
|
|
|
const rawBody = String(cont.body || '').trim()
|
|
const rawBody = String(cont.body || '').trim()
|
|
|
if (cont.fileName && (!rawBody || /^\[文件\]/.test(rawBody))) {
|
|
if (cont.fileName && (!rawBody || /^\[文件\]/.test(rawBody))) {
|
|
|
- this.appendMessage({
|
|
|
|
|
|
|
+ return pushMessage({
|
|
|
type: 'file',
|
|
type: 'file',
|
|
|
direction,
|
|
direction,
|
|
|
department: messageMeta.department,
|
|
department: messageMeta.department,
|
|
@@ -1465,9 +1630,8 @@ const miniprogramState = (() => {
|
|
|
receiptStatus,
|
|
receiptStatus,
|
|
|
msgId
|
|
msgId
|
|
|
})
|
|
})
|
|
|
- return
|
|
|
|
|
}
|
|
}
|
|
|
- this.appendMessage(createTextMessage({
|
|
|
|
|
|
|
+ return pushMessage(createTextMessage({
|
|
|
direction,
|
|
direction,
|
|
|
department: messageMeta.department,
|
|
department: messageMeta.department,
|
|
|
name: messageMeta.name,
|
|
name: messageMeta.name,
|
|
@@ -1480,11 +1644,10 @@ const miniprogramState = (() => {
|
|
|
time: displayTime,
|
|
time: displayTime,
|
|
|
msgId
|
|
msgId
|
|
|
}))
|
|
}))
|
|
|
- return
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (typeCode === '122') {
|
|
if (typeCode === '122') {
|
|
|
- this.appendMessage(createImageMessage({
|
|
|
|
|
|
|
+ return pushMessage(createImageMessage({
|
|
|
direction,
|
|
direction,
|
|
|
department: messageMeta.department,
|
|
department: messageMeta.department,
|
|
|
name: messageMeta.name,
|
|
name: messageMeta.name,
|
|
@@ -1497,11 +1660,10 @@ const miniprogramState = (() => {
|
|
|
receiptStatus,
|
|
receiptStatus,
|
|
|
msgId
|
|
msgId
|
|
|
}))
|
|
}))
|
|
|
- return
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (typeCode === '123') {
|
|
if (typeCode === '123') {
|
|
|
- this.appendMessage({
|
|
|
|
|
|
|
+ return pushMessage({
|
|
|
type: 'voice',
|
|
type: 'voice',
|
|
|
direction,
|
|
direction,
|
|
|
department: messageMeta.department,
|
|
department: messageMeta.department,
|
|
@@ -1518,11 +1680,10 @@ const miniprogramState = (() => {
|
|
|
time: displayTime,
|
|
time: displayTime,
|
|
|
msgId
|
|
msgId
|
|
|
})
|
|
})
|
|
|
- return
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (typeCode === '124') {
|
|
if (typeCode === '124') {
|
|
|
- this.appendMessage({
|
|
|
|
|
|
|
+ return pushMessage({
|
|
|
type: 'video',
|
|
type: 'video',
|
|
|
direction,
|
|
direction,
|
|
|
department: messageMeta.department,
|
|
department: messageMeta.department,
|
|
@@ -1539,15 +1700,46 @@ const miniprogramState = (() => {
|
|
|
msgId
|
|
msgId
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
+ return false
|
|
|
},
|
|
},
|
|
|
handleWsIncomingMessage(payload = {}) {
|
|
handleWsIncomingMessage(payload = {}) {
|
|
|
this.appendPayloadMessage(payload, { fromHistory: false })
|
|
this.appendPayloadMessage(payload, { fromHistory: false })
|
|
|
},
|
|
},
|
|
|
handleWsHistoryMessage(payload = {}) {
|
|
handleWsHistoryMessage(payload = {}) {
|
|
|
- this.appendPayloadMessage(payload, { fromHistory: true })
|
|
|
|
|
|
|
+ const inserted = this.appendPayloadMessage(payload, {
|
|
|
|
|
+ fromHistory: true,
|
|
|
|
|
+ prepend: this.historyMode === 'older',
|
|
|
|
|
+ collectOnly: this.historyMode === 'older'
|
|
|
|
|
+ })
|
|
|
|
|
+ if (inserted) {
|
|
|
|
|
+ this.historyBatchCount += 1
|
|
|
|
|
+ }
|
|
|
},
|
|
},
|
|
|
- handleWsHistoryDone() {
|
|
|
|
|
|
|
+ handleWsHistoryDone(payload = {}) {
|
|
|
if (!this.historyLoading) return
|
|
if (!this.historyLoading) return
|
|
|
|
|
+ const pkgNum = Number(payload?.pkgNum || 0)
|
|
|
|
|
+ const pageNo = Number(payload?.pageNo || 0)
|
|
|
|
|
+ console.log('[History] done:', {
|
|
|
|
|
+ historyMode: this.historyMode,
|
|
|
|
|
+ historyBatchCount: this.historyBatchCount,
|
|
|
|
|
+ pkgNum,
|
|
|
|
|
+ pageNo,
|
|
|
|
|
+ historyPageNo: this.historyPageNo
|
|
|
|
|
+ })
|
|
|
|
|
+ if (this.historyMode === 'older') {
|
|
|
|
|
+ if (this.historyBatchCount > 0) {
|
|
|
|
|
+ if (this.historyPendingMessages.length) {
|
|
|
|
|
+ this.prependMessages(this.historyPendingMessages)
|
|
|
|
|
+ }
|
|
|
|
|
+ this.scrollIntoView = `msg-${this.historyBatchCount}`
|
|
|
|
|
+ this.historyPageNo += 1
|
|
|
|
|
+ }
|
|
|
|
|
+ if (pkgNum > 0) {
|
|
|
|
|
+ this.hasMoreHistory = pkgNum >= 20
|
|
|
|
|
+ } else if (this.historyBatchCount < 20) {
|
|
|
|
|
+ this.hasMoreHistory = false
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
this.finishHistoryLoading()
|
|
this.finishHistoryLoading()
|
|
|
},
|
|
},
|
|
|
shouldHandleIncomingPayload(payload = {}) {
|
|
shouldHandleIncomingPayload(payload = {}) {
|
|
@@ -1556,6 +1748,7 @@ const miniprogramState = (() => {
|
|
|
const me = String(this.wsIdentity.sendRyid || '')
|
|
const me = String(this.wsIdentity.sendRyid || '')
|
|
|
const peer = String(this.wsIdentity.recRyid || '')
|
|
const peer = String(this.wsIdentity.recRyid || '')
|
|
|
if (!me || !peer) return true
|
|
if (!me || !peer) return true
|
|
|
|
|
+ if (!send || !rec) return true
|
|
|
return (send === me && rec === peer) || (send === peer && rec === me)
|
|
return (send === me && rec === peer) || (send === peer && rec === me)
|
|
|
},
|
|
},
|
|
|
handleWsReceipt(payload = {}) {
|
|
handleWsReceipt(payload = {}) {
|
|
@@ -1610,6 +1803,16 @@ const miniprogramState = (() => {
|
|
|
},
|
|
},
|
|
|
async sendOutgoingByType(message) {
|
|
async sendOutgoingByType(message) {
|
|
|
if (!message) return false
|
|
if (!message) return false
|
|
|
|
|
+ const typeCodeMap = {
|
|
|
|
|
+ text: 121,
|
|
|
|
|
+ image: 122,
|
|
|
|
|
+ voice: 123,
|
|
|
|
|
+ video: 124
|
|
|
|
|
+ }
|
|
|
|
|
+ const featureCode = typeCodeMap[message.type]
|
|
|
|
|
+ if (featureCode && !this.ensureFeatureAvailable(featureCode)) {
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
const sendRyid = this.wsIdentity.sendRyid
|
|
const sendRyid = this.wsIdentity.sendRyid
|
|
|
const recRyid = this.wsIdentity.recRyid
|
|
const recRyid = this.wsIdentity.recRyid
|
|
|
if (!sendRyid || !recRyid) {
|
|
if (!sendRyid || !recRyid) {
|
|
@@ -1823,8 +2026,85 @@ const miniprogramState = (() => {
|
|
|
this.isRecording = false
|
|
this.isRecording = false
|
|
|
this.recordSeconds = 0
|
|
this.recordSeconds = 0
|
|
|
},
|
|
},
|
|
|
- handlePressToTalkStart(event) {
|
|
|
|
|
|
|
+ async ensureRecordPermission() {
|
|
|
|
|
+ if (this.recordPermissionPromise) return this.recordPermissionPromise
|
|
|
|
|
+ const permissionPromise = new Promise((resolve) => {
|
|
|
|
|
+ uni.getSetting({
|
|
|
|
|
+ success: (settingRes) => {
|
|
|
|
|
+ const authSetting = settingRes && settingRes.authSetting ? settingRes.authSetting : {}
|
|
|
|
|
+ const hasRecordAuth = authSetting['scope.record']
|
|
|
|
|
+ console.log('[RecordPermission] getSetting authSetting:', authSetting, 'scope.record =', hasRecordAuth)
|
|
|
|
|
+ if (hasRecordAuth === true) {
|
|
|
|
|
+ console.log('[RecordPermission] already authorized')
|
|
|
|
|
+ resolve(true)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ if (hasRecordAuth === false) {
|
|
|
|
|
+ console.log('[RecordPermission] previously denied, openSetting required')
|
|
|
|
|
+ uni.showModal({
|
|
|
|
|
+ title: '提示',
|
|
|
|
|
+ content: '请先开启录音权限后再使用语音留言',
|
|
|
|
|
+ confirmText: '去授权',
|
|
|
|
|
+ cancelText: '取消',
|
|
|
|
|
+ success: (modalRes) => {
|
|
|
|
|
+ if (!modalRes.confirm) {
|
|
|
|
|
+ resolve(false)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ uni.openSetting({
|
|
|
|
|
+ success: (openRes) => {
|
|
|
|
|
+ const openAuthSetting = openRes && openRes.authSetting ? openRes.authSetting : {}
|
|
|
|
|
+ console.log('[RecordPermission] openSetting authSetting:', openAuthSetting, 'scope.record =', openAuthSetting['scope.record'])
|
|
|
|
|
+ resolve(!!openAuthSetting['scope.record'])
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: (error) => {
|
|
|
|
|
+ console.error('[RecordPermission] openSetting fail:', error)
|
|
|
|
|
+ resolve(false)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: (error) => {
|
|
|
|
|
+ console.error('[RecordPermission] showModal fail:', error)
|
|
|
|
|
+ resolve(false)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ uni.authorize({
|
|
|
|
|
+ scope: 'scope.record',
|
|
|
|
|
+ success: () => {
|
|
|
|
|
+ console.log('[RecordPermission] authorize success')
|
|
|
|
|
+ resolve(true)
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: () => {
|
|
|
|
|
+ console.warn('[RecordPermission] authorize denied')
|
|
|
|
|
+ uni.showToast({ title: '请先开启录音权限', icon: 'none' })
|
|
|
|
|
+ resolve(false)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: (error) => {
|
|
|
|
|
+ console.error('[RecordPermission] getSetting fail:', error)
|
|
|
|
|
+ resolve(false)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+ this.recordPermissionPromise = permissionPromise.finally(() => {
|
|
|
|
|
+ this.recordPermissionPromise = null
|
|
|
|
|
+ })
|
|
|
|
|
+ return this.recordPermissionPromise
|
|
|
|
|
+ },
|
|
|
|
|
+ handlePressToTalkTouchStart() {
|
|
|
if (this.isRecording) return
|
|
if (this.isRecording) return
|
|
|
|
|
+ this.isPressingToTalk = true
|
|
|
|
|
+ this.ensureRecordPermission().catch(() => {})
|
|
|
|
|
+ },
|
|
|
|
|
+ async handlePressToTalkStart(event) {
|
|
|
|
|
+ if (this.isRecording) return
|
|
|
|
|
+ if (!this.ensureFeatureAvailable(123)) return
|
|
|
|
|
+ const hasRecordPermission = await this.ensureRecordPermission()
|
|
|
|
|
+ if (!hasRecordPermission) return
|
|
|
|
|
+ if (!this.isPressingToTalk) return
|
|
|
if (!this.recorderManager) {
|
|
if (!this.recorderManager) {
|
|
|
this.initRecorder()
|
|
this.initRecorder()
|
|
|
}
|
|
}
|
|
@@ -1871,10 +2151,12 @@ const miniprogramState = (() => {
|
|
|
this.isRecordCancel = deltaY > 80
|
|
this.isRecordCancel = deltaY > 80
|
|
|
},
|
|
},
|
|
|
handlePressToTalkEnd() {
|
|
handlePressToTalkEnd() {
|
|
|
|
|
+ this.isPressingToTalk = false
|
|
|
if (!this.isRecording || !this.recorderManager) return
|
|
if (!this.isRecording || !this.recorderManager) return
|
|
|
this.recorderManager.stop()
|
|
this.recorderManager.stop()
|
|
|
},
|
|
},
|
|
|
handlePressToTalkCancel() {
|
|
handlePressToTalkCancel() {
|
|
|
|
|
+ this.isPressingToTalk = false
|
|
|
if (!this.isRecording || !this.recorderManager) return
|
|
if (!this.isRecording || !this.recorderManager) return
|
|
|
this.isRecordCancel = true
|
|
this.isRecordCancel = true
|
|
|
this.recorderManager.stop()
|
|
this.recorderManager.stop()
|
|
@@ -1942,6 +2224,7 @@ const miniprogramState = (() => {
|
|
|
},
|
|
},
|
|
|
toggleInputMode() {
|
|
toggleInputMode() {
|
|
|
if (this.inputMode === 'voice') {
|
|
if (this.inputMode === 'voice') {
|
|
|
|
|
+ if (!this.ensureFeatureAvailable(121)) return
|
|
|
this.inputMode = 'text'
|
|
this.inputMode = 'text'
|
|
|
this.showMorePanel = false
|
|
this.showMorePanel = false
|
|
|
this.showEmojiPanel = false
|
|
this.showEmojiPanel = false
|
|
@@ -2001,31 +2284,69 @@ const miniprogramState = (() => {
|
|
|
this.listTouchMoved = true
|
|
this.listTouchMoved = true
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
- handleListTouchEnd() {
|
|
|
|
|
- this.resetInactivityTimer()
|
|
|
|
|
- if (!this.listTouchMoved) {
|
|
|
|
|
- this.handlePageClick()
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
- appendMessages(newMessages = []) {
|
|
|
|
|
- if (!newMessages.length) return
|
|
|
|
|
- const startIndex = this.messages.length
|
|
|
|
|
- this.messages = [...this.messages, ...newMessages]
|
|
|
|
|
- newMessages.forEach((message, offset) => {
|
|
|
|
|
- if (message.showReceiptToggle) {
|
|
|
|
|
- this.registerReceiptToggleTimer(startIndex + offset)
|
|
|
|
|
|
|
+ handleListTouchEnd() {
|
|
|
|
|
+ this.resetInactivityTimer()
|
|
|
|
|
+ if (!this.listTouchMoved) {
|
|
|
|
|
+ this.handlePageClick()
|
|
|
}
|
|
}
|
|
|
- })
|
|
|
|
|
- this.$nextTick(() => {
|
|
|
|
|
- this.scrollToBottom()
|
|
|
|
|
- })
|
|
|
|
|
- },
|
|
|
|
|
- appendMessage(newMessage) {
|
|
|
|
|
- if (!newMessage) return
|
|
|
|
|
- this.appendMessages([newMessage])
|
|
|
|
|
- },
|
|
|
|
|
|
|
+ },
|
|
|
|
|
+ handleMessageListScroll(event) {
|
|
|
|
|
+ const scrollTop = Number(event?.detail?.scrollTop || 0)
|
|
|
|
|
+ if (scrollTop > 180) {
|
|
|
|
|
+ this.historyTopLoadArmed = true
|
|
|
|
|
+ }
|
|
|
|
|
+ if (scrollTop > 120) return
|
|
|
|
|
+ this.handleHistoryReachTop()
|
|
|
|
|
+ },
|
|
|
|
|
+ async handleHistoryReachTop() {
|
|
|
|
|
+ this.resetInactivityTimer()
|
|
|
|
|
+ if (!this.historyTopLoadArmed) return
|
|
|
|
|
+ if (this.historyLoading || this.loadingMoreHistory) return
|
|
|
|
|
+ if (!this.hasMoreHistory) return
|
|
|
|
|
+ if (!this.messages.length) return
|
|
|
|
|
+ this.historyTopLoadArmed = false
|
|
|
|
|
+ await this.loadHistoryMessages({
|
|
|
|
|
+ reset: false,
|
|
|
|
|
+ cmd: HISTORY_MORE_CMD,
|
|
|
|
|
+ mode: 'older'
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+ prependMessages(newMessages = []) {
|
|
|
|
|
+ if (!newMessages.length) return
|
|
|
|
|
+ this.messages = [...newMessages, ...this.messages]
|
|
|
|
|
+ },
|
|
|
|
|
+ prependMessage(newMessage) {
|
|
|
|
|
+ if (!newMessage) return
|
|
|
|
|
+ this.prependMessages([newMessage])
|
|
|
|
|
+ },
|
|
|
|
|
+ appendMessages(newMessages = []) {
|
|
|
|
|
+ if (!newMessages.length) return
|
|
|
|
|
+ const startIndex = this.messages.length
|
|
|
|
|
+ this.messages = [...this.messages, ...newMessages]
|
|
|
|
|
+ newMessages.forEach((message, offset) => {
|
|
|
|
|
+ if (message.showReceiptToggle) {
|
|
|
|
|
+ this.registerReceiptToggleTimer(startIndex + offset)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ this.$nextTick(() => {
|
|
|
|
|
+ this.scrollToBottom()
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+ appendMessage(newMessage) {
|
|
|
|
|
+ if (!newMessage) return
|
|
|
|
|
+ this.appendMessages([newMessage])
|
|
|
|
|
+ },
|
|
|
async handleMoreAction(type) {
|
|
async handleMoreAction(type) {
|
|
|
try {
|
|
try {
|
|
|
|
|
+ const typeCodeMap = {
|
|
|
|
|
+ image: 122,
|
|
|
|
|
+ camera: 122,
|
|
|
|
|
+ video: 124
|
|
|
|
|
+ }
|
|
|
|
|
+ const featureCode = typeCodeMap[type]
|
|
|
|
|
+ if (featureCode && !this.ensureFeatureAvailable(featureCode)) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
if (type === 'file') {
|
|
if (type === 'file') {
|
|
|
const fileMessage = await pickFileOutgoingMessage(this.currentUserMeta)
|
|
const fileMessage = await pickFileOutgoingMessage(this.currentUserMeta)
|
|
|
if (!fileMessage) return
|
|
if (!fileMessage) return
|
|
@@ -2191,6 +2512,11 @@ const miniprogramState = (() => {
|
|
|
},
|
|
},
|
|
|
handleTextLineChange(event) {
|
|
handleTextLineChange(event) {
|
|
|
this.textLineCount = event.detail && event.detail.lineCount ? event.detail.lineCount : 1
|
|
this.textLineCount = event.detail && event.detail.lineCount ? event.detail.lineCount : 1
|
|
|
|
|
+ },
|
|
|
|
|
+ handleTextInputFocus() {
|
|
|
|
|
+ if (this.ensureFeatureAvailable(121)) return
|
|
|
|
|
+ this.inputFocus = false
|
|
|
|
|
+ uni.hideKeyboard && uni.hideKeyboard()
|
|
|
},
|
|
},
|
|
|
async sendTextMessage() {
|
|
async sendTextMessage() {
|
|
|
const message = buildTextOutgoingMessage(this.draftText, this.currentUserMeta)
|
|
const message = buildTextOutgoingMessage(this.draftText, this.currentUserMeta)
|
|
@@ -2290,13 +2616,20 @@ const miniprogramState = (() => {
|
|
|
display: block;
|
|
display: block;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- .message-list {
|
|
|
|
|
- flex: 1;
|
|
|
|
|
- padding: 20rpx;
|
|
|
|
|
- background: #fff;
|
|
|
|
|
- overflow-y: auto;
|
|
|
|
|
- box-sizing: border-box;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ .message-list {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ padding: 20rpx;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .history-loading-tip {
|
|
|
|
|
+ padding: 12rpx 0 6rpx;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ font-size: 24rpx;
|
|
|
|
|
+ color: #9ca3af;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
.message-item {
|
|
.message-item {
|
|
|
display: flex;
|
|
display: flex;
|
|
@@ -2665,19 +2998,20 @@ const miniprogramState = (() => {
|
|
|
border-radius: 4rpx;
|
|
border-radius: 4rpx;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- .more-grid {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: flex-start;
|
|
|
|
|
- justify-content: space-between;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ .more-grid {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
|
|
|
+ column-gap: 0;
|
|
|
|
|
+ row-gap: 0;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- .more-item {
|
|
|
|
|
- width: 25%;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: column;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- gap: 16rpx;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ .more-item {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 16rpx;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
.more-icon {
|
|
.more-icon {
|
|
|
width: 96rpx;
|
|
width: 96rpx;
|
|
@@ -2878,5 +3212,20 @@ const miniprogramState = (() => {
|
|
|
color: #ffb2b2;
|
|
color: #ffb2b2;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ .service-confirm-body {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+ padding: 24rpx 32rpx;
|
|
|
|
|
+ font-size: 30rpx;
|
|
|
|
|
+ color: #333333;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ line-height: 1.6;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
}
|
|
}
|
|
|
</style>
|
|
</style>
|