|
|
@@ -1,6 +1,6 @@
|
|
|
<template>
|
|
|
<view class="message-page">
|
|
|
- <view v-if="sessionRole === 'parent'" class="chat-nav">
|
|
|
+ <view class="chat-nav">
|
|
|
<view class="chat-title-wrap">
|
|
|
<picker
|
|
|
v-if="parentChatMembers.length > 1"
|
|
|
@@ -10,7 +10,7 @@
|
|
|
@change="handleParentMemberChange"
|
|
|
>
|
|
|
<view class="chat-title-picker">
|
|
|
- <text class="chat-name-arrow">▼</text>
|
|
|
+ <Icon lib="base" name="icon-down" size="36" color="#000000" />
|
|
|
<text class="chat-title">{{ currentChatName }}</text>
|
|
|
</view>
|
|
|
</picker>
|
|
|
@@ -25,6 +25,9 @@
|
|
|
<view class="chat-call-btn" @click="startVideoCall">
|
|
|
<Icon lib="base" name="icon-vid-bold" size="36" color="#3c4151" />
|
|
|
</view>
|
|
|
+ <view v-if="sessionRole === 'device'" class="chat-call-btn" @click="handleDeviceLogout">
|
|
|
+ <image class="chat-logout-icon" src="/static/icon/logout.png"></image>
|
|
|
+ </view>
|
|
|
</view>
|
|
|
</view>
|
|
|
|
|
|
@@ -35,14 +38,22 @@
|
|
|
:id="'msg-' + index">
|
|
|
<!-- 头像+时间区域 -->
|
|
|
<view class="avatar-section">
|
|
|
- <image class="avatar" src="/static/logo.png"></image>
|
|
|
- <text class="msg-time">{{ message.time }}</text>
|
|
|
+ <image
|
|
|
+ class="avatar"
|
|
|
+ :class="getAvatarClass(message)"
|
|
|
+ :src="getAvatarSrc(message)"
|
|
|
+ mode="aspectFill"
|
|
|
+ ></image>
|
|
|
+ <text class="msg-time">{{ message.time || '--:--' }}</text>
|
|
|
</view>
|
|
|
|
|
|
<!-- 内容区域 -->
|
|
|
<view class="content-section">
|
|
|
- <!-- 部门/班级 | 姓名 -->
|
|
|
- <view class="user-info">{{ message.department }} | {{ message.name }}</view>
|
|
|
+ <!-- 职务 | 姓名(职务先隐藏) -->
|
|
|
+ <view class="user-info" :class="{ 'user-info-placeholder': message.direction === 'right' }">
|
|
|
+ <text class="user-info-role">{{ message.department }} | </text>
|
|
|
+ <text>{{ message.direction === 'left' ? (message.displayName || message.name) : '占位' }}</text>
|
|
|
+ </view>
|
|
|
|
|
|
<!-- 消息内容 -->
|
|
|
<view class="message-content">
|
|
|
@@ -149,7 +160,7 @@
|
|
|
@touchend.stop.prevent="handlePressToTalkEnd"
|
|
|
@touchcancel.stop.prevent="handlePressToTalkCancel"
|
|
|
>
|
|
|
- <Icon lib="base" name="icon-aud-bold" size="36" :color="iconColor" />
|
|
|
+ <Icon lib="base" name="icon-aud" size="36" :color="iconColor" />
|
|
|
<text class="press-text">长按说话</text>
|
|
|
</view>
|
|
|
<textarea
|
|
|
@@ -194,7 +205,7 @@
|
|
|
</view>
|
|
|
<view class="more-item" @click="handleMoreAction('camera')">
|
|
|
<view class="more-icon">
|
|
|
- <Icon lib="base" name="icon-vid-bold" size="36" :color="iconColor" />
|
|
|
+ <Icon lib="base" name="icon-photo" size="36" :color="iconColor" />
|
|
|
</view>
|
|
|
<text class="more-text">拍照</text>
|
|
|
</view>
|
|
|
@@ -265,11 +276,22 @@
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
+
|
|
|
+ <custom-modal
|
|
|
+ :visible="modalVisible"
|
|
|
+ :title="modalTitle"
|
|
|
+ :content="modalContent"
|
|
|
+ :showCancel="modalShowCancel"
|
|
|
+ :confirmText="modalConfirmText"
|
|
|
+ @confirm="handleModalConfirm"
|
|
|
+ @cancel="handleModalCancel"
|
|
|
+ />
|
|
|
</view>
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
import Icon from '@/components/icon/index.vue'
|
|
|
+import customModal from '@/components/custom-modal.vue'
|
|
|
import { EMOJI_LIST } from '@/constants/emoji'
|
|
|
import { collectImageUrls, createImageMessage, createTextMessage } from '@/utils/parent-message-factory'
|
|
|
import {
|
|
|
@@ -284,6 +306,7 @@ import websocketService from '@/utils/websocket'
|
|
|
import upload from '@/utils/upload'
|
|
|
import { getImageUrl } from '@/utils/util'
|
|
|
import { grfwApi } from '@/api/grfw'
|
|
|
+import { deviceApi } from '@/api/device'
|
|
|
import env from '@/config/env.js'
|
|
|
|
|
|
const RECEIPT_TOGGLE_TTL_MS = 60 * 1000
|
|
|
@@ -323,13 +346,18 @@ const miniprogramState = (() => {
|
|
|
type: [String, Number],
|
|
|
default: ''
|
|
|
},
|
|
|
+ contactName: {
|
|
|
+ type: String,
|
|
|
+ default: ''
|
|
|
+ },
|
|
|
studentId: {
|
|
|
type: [String, Number],
|
|
|
default: ''
|
|
|
}
|
|
|
},
|
|
|
components: {
|
|
|
- Icon
|
|
|
+ Icon,
|
|
|
+ customModal
|
|
|
},
|
|
|
computed: {},
|
|
|
watch: {
|
|
|
@@ -341,6 +369,9 @@ const miniprogramState = (() => {
|
|
|
this.initializeSessionFromOptions({ force: true, role: this.role, contactId: this.contactId, studentId: this.studentId })
|
|
|
this.bootstrapMessageFlow()
|
|
|
},
|
|
|
+ contactName() {
|
|
|
+ this.initializeSessionFromOptions({ force: true, role: this.role, contactId: this.contactId, contactName: this.contactName, studentId: this.studentId })
|
|
|
+ },
|
|
|
studentId() {
|
|
|
this.initializeSessionFromOptions({ force: true, role: this.role, contactId: this.contactId, studentId: this.studentId })
|
|
|
this.bootstrapMessageFlow()
|
|
|
@@ -384,7 +415,7 @@ const miniprogramState = (() => {
|
|
|
currentUserMeta: {
|
|
|
direction: 'right',
|
|
|
department: '家长',
|
|
|
- name: '我'
|
|
|
+ name: '家长'
|
|
|
},
|
|
|
textLineCount: 1,
|
|
|
listTouchMoved: false,
|
|
|
@@ -403,6 +434,7 @@ const miniprogramState = (() => {
|
|
|
sessionRole: 'parent',
|
|
|
sessionContactId: '',
|
|
|
sessionStudentId: '',
|
|
|
+ entryFromDeviceIndex: false,
|
|
|
socketConnected: false,
|
|
|
socketUnsubscribeList: [],
|
|
|
currentWsConfig: null,
|
|
|
@@ -411,6 +443,19 @@ const miniprogramState = (() => {
|
|
|
recRyid: '',
|
|
|
recRylbm: REC_RYLBM_STUDENT
|
|
|
},
|
|
|
+ deviceSessionReady: true,
|
|
|
+ deviceInitPromise: null,
|
|
|
+ peerAvatarTypeHint: '',
|
|
|
+ serviceStatus: null,
|
|
|
+ inactivityTimer: null,
|
|
|
+ inactivityTimeout: 180000,
|
|
|
+ modalVisible: false,
|
|
|
+ modalTitle: '提示',
|
|
|
+ modalContent: '',
|
|
|
+ modalShowCancel: false,
|
|
|
+ modalConfirmText: '确定',
|
|
|
+ modalResolve: null,
|
|
|
+ loadOptions: {},
|
|
|
// 消息列表数据
|
|
|
messages: []
|
|
|
}
|
|
|
@@ -419,14 +464,21 @@ const miniprogramState = (() => {
|
|
|
this.initializeSessionFromOptions({
|
|
|
role: this.role,
|
|
|
contactId: this.contactId,
|
|
|
+ contactName: this.contactName,
|
|
|
studentId: this.studentId
|
|
|
})
|
|
|
},
|
|
|
- onLoad(options) {
|
|
|
+ async onLoad(options) {
|
|
|
+ this.loadOptions = { ...(options || {}) }
|
|
|
this.initializeSessionFromOptions({ ...(options || {}), force: true })
|
|
|
this.initParentInfo()
|
|
|
+ if (this.sessionRole === 'device') {
|
|
|
+ this.deviceSessionReady = false
|
|
|
+ this.deviceInitPromise = this.initDeviceSession(options || {})
|
|
|
+ this.deviceSessionReady = await this.deviceInitPromise
|
|
|
+ }
|
|
|
},
|
|
|
- onReady() {
|
|
|
+ async onReady() {
|
|
|
// 页面渲染完成后,滚动到最新消息
|
|
|
this.$nextTick(() => {
|
|
|
this.scrollToBottom()
|
|
|
@@ -435,7 +487,14 @@ const miniprogramState = (() => {
|
|
|
})
|
|
|
this.registerVoipEvent()
|
|
|
this.bindSocketListeners()
|
|
|
- this.bootstrapMessageFlow()
|
|
|
+ if (this.sessionRole === 'device') {
|
|
|
+ await (this.deviceInitPromise || Promise.resolve(true))
|
|
|
+ if (!this.deviceSessionReady) return
|
|
|
+ await this.checkServiceStatus(115)
|
|
|
+ await this.checkServiceStatus(111)
|
|
|
+ this.resetInactivityTimer()
|
|
|
+ }
|
|
|
+ await this.bootstrapMessageFlow()
|
|
|
},
|
|
|
onUnload() {
|
|
|
this.teardownSocket()
|
|
|
@@ -446,22 +505,62 @@ const miniprogramState = (() => {
|
|
|
}
|
|
|
this.cleanupRecorder()
|
|
|
this.clearReceiptToggleTimers()
|
|
|
+ if (this.inactivityTimer) {
|
|
|
+ clearTimeout(this.inactivityTimer)
|
|
|
+ this.inactivityTimer = null
|
|
|
+ }
|
|
|
},
|
|
|
beforeUnmount() {
|
|
|
this.teardownSocket()
|
|
|
},
|
|
|
methods: {
|
|
|
+ showCustomModal(options = {}) {
|
|
|
+ return new Promise((resolve) => {
|
|
|
+ this.modalTitle = options.title || '提示'
|
|
|
+ this.modalContent = options.content || ''
|
|
|
+ this.modalShowCancel = options.showCancel || false
|
|
|
+ this.modalConfirmText = options.confirmText || '确定'
|
|
|
+ this.modalResolve = resolve
|
|
|
+ this.modalVisible = true
|
|
|
+ })
|
|
|
+ },
|
|
|
+ handleModalConfirm() {
|
|
|
+ this.modalVisible = false
|
|
|
+ if (this.modalResolve) {
|
|
|
+ this.modalResolve(true)
|
|
|
+ this.modalResolve = null
|
|
|
+ }
|
|
|
+ },
|
|
|
+ handleModalCancel() {
|
|
|
+ this.modalVisible = false
|
|
|
+ if (this.modalResolve) {
|
|
|
+ this.modalResolve(false)
|
|
|
+ this.modalResolve = null
|
|
|
+ }
|
|
|
+ },
|
|
|
initializeSessionFromOptions(options = {}) {
|
|
|
if (this.sessionInitialized && !options?.force) return
|
|
|
+ const safeDecode = (value) => {
|
|
|
+ if (value === null || value === undefined) return ''
|
|
|
+ const text = String(value)
|
|
|
+ try {
|
|
|
+ return decodeURIComponent(text)
|
|
|
+ } catch (error) {
|
|
|
+ return text
|
|
|
+ }
|
|
|
+ }
|
|
|
const role = options.role || this.role || 'parent'
|
|
|
this.sessionRole = role
|
|
|
- this.sessionContactId = options.contactId || this.contactId || ''
|
|
|
+ this.sessionContactId = safeDecode(options.contactId || this.contactId || '')
|
|
|
this.sessionStudentId = options.studentId || this.studentId || ''
|
|
|
- this.currentChatName = role === 'device' ? '家长' : '留言'
|
|
|
+ this.entryFromDeviceIndex = String(options.fromIndex || '') === '1'
|
|
|
+ const targetContactName = safeDecode(options.contactName || this.contactName || '')
|
|
|
+ this.currentChatName = role === 'device' ? (targetContactName || '家长') : '留言'
|
|
|
this.currentUserMeta = {
|
|
|
...this.currentUserMeta,
|
|
|
department: role === 'device' ? '设备端' : '家长'
|
|
|
}
|
|
|
+ this.refreshCurrentUserMeta()
|
|
|
this.resolveWsIdentity()
|
|
|
this.currentWsConfig = this.buildWsConnectOptions()
|
|
|
this.sessionInitialized = true
|
|
|
@@ -473,9 +572,65 @@ const miniprogramState = (() => {
|
|
|
if (this.sessionRole === 'parent') {
|
|
|
await this.loadParentChatMembers()
|
|
|
} else {
|
|
|
+ await this.loadDeviceChatMembers()
|
|
|
await this.ensureSessionSocket()
|
|
|
}
|
|
|
await this.loadHistoryMessages({ reset: true })
|
|
|
+ if (this.sessionRole === 'device' && this.entryFromDeviceIndex) {
|
|
|
+ await this.loadHistoryMessages({ reset: true })
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async initDeviceSession(options = {}) {
|
|
|
+ if (this.sessionRole !== 'device') return
|
|
|
+ const userInfo = this.getStoredUserInfo()
|
|
|
+ const snFromOption = String(options.sn || '').trim()
|
|
|
+ const cardNo = String(options.cardNo || '').trim()
|
|
|
+ const sn = snFromOption || String(userInfo.devId || '').trim()
|
|
|
+ if (!sn || !cardNo) return false
|
|
|
+ try {
|
|
|
+ const result = await deviceApi.login(sn, cardNo)
|
|
|
+ const loginData = result?.data || {}
|
|
|
+ if (!loginData || loginData.msg) {
|
|
|
+ await this.handleDeviceLoginFailure('登录失败,卡无登记')
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ const mergedUserInfo = {
|
|
|
+ ...this.getStoredUserInfo(),
|
|
|
+ devId: loginData.devId || sn,
|
|
|
+ sbmc: loginData.sbmc || '',
|
|
|
+ sessId: loginData.sessId || '',
|
|
|
+ xm: loginData.xm || '',
|
|
|
+ yhsbToken: loginData.yhsbToken || '',
|
|
|
+ onlineToken: loginData.onlineToken || '',
|
|
|
+ syList: loginData.sylist || loginData.syList || [],
|
|
|
+ yhid: loginData.yhid || '',
|
|
|
+ yhm: loginData.yhm || ''
|
|
|
+ }
|
|
|
+ if (loginData.yszwj) mergedUserInfo.yszwj = loginData.yszwj
|
|
|
+ if (loginData.zjzwj) mergedUserInfo.zjzwj = loginData.zjzwj
|
|
|
+ uni.setStorageSync('userInfo', mergedUserInfo)
|
|
|
+ if (mergedUserInfo.sessId) {
|
|
|
+ uni.setStorageSync('JSESSIONID', mergedUserInfo.sessId)
|
|
|
+ }
|
|
|
+ this.refreshCurrentUserMeta()
|
|
|
+ this.resolveWsIdentity()
|
|
|
+ this.currentWsConfig = this.buildWsConnectOptions()
|
|
|
+ return true
|
|
|
+ } catch (error) {
|
|
|
+ console.error('设备端登录失败:', error)
|
|
|
+ await this.handleDeviceLoginFailure('网络错误或服务异常')
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async handleDeviceLoginFailure(content = '登录失败') {
|
|
|
+ await this.showCustomModal({
|
|
|
+ title: '登录失败',
|
|
|
+ content,
|
|
|
+ showCancel: false,
|
|
|
+ confirmText: '确定'
|
|
|
+ })
|
|
|
+ await this.handleDeviceLogout()
|
|
|
+ return false
|
|
|
},
|
|
|
async loadParentChatMembers() {
|
|
|
try {
|
|
|
@@ -503,6 +658,31 @@ const miniprogramState = (() => {
|
|
|
uni.showToast({ title: '加载孩子列表失败', icon: 'none' })
|
|
|
}
|
|
|
},
|
|
|
+ async loadDeviceChatMembers() {
|
|
|
+ try {
|
|
|
+ const result = await deviceApi.grfw_selChatMbr()
|
|
|
+ const data = result?.data || {}
|
|
|
+ const list = this.normalizeChatMemberList(data.chatMbrList)
|
|
|
+ this.parentChatMembers = list
|
|
|
+ this.parentChatMemberNames = list.map((item) => item.xm || String(item.ryid || '未命名家长'))
|
|
|
+ if (!list.length) {
|
|
|
+ this.sessionContactId = ''
|
|
|
+ this.currentChatName = '家长'
|
|
|
+ this.resolveWsIdentity()
|
|
|
+ return
|
|
|
+ }
|
|
|
+ let targetIndex = 0
|
|
|
+ const current = String(this.sessionContactId || '')
|
|
|
+ if (current) {
|
|
|
+ const idx = list.findIndex((item) => String(item.ryid || '') === current)
|
|
|
+ if (idx > -1) targetIndex = idx
|
|
|
+ }
|
|
|
+ this.selectParentMemberByIndex(targetIndex)
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载家长列表失败:', error)
|
|
|
+ uni.showToast({ title: '加载家长列表失败', icon: 'none' })
|
|
|
+ }
|
|
|
+ },
|
|
|
normalizeChatMemberList(rawList) {
|
|
|
if (!Array.isArray(rawList)) return []
|
|
|
const queue = [...rawList]
|
|
|
@@ -519,10 +699,14 @@ const miniprogramState = (() => {
|
|
|
}
|
|
|
return result
|
|
|
},
|
|
|
- handleParentMemberChange(event) {
|
|
|
+ async handleParentMemberChange(event) {
|
|
|
+ if (this.historyLoading) {
|
|
|
+ await this.finishHistoryLoading()
|
|
|
+ }
|
|
|
const index = Number(event?.detail?.value || 0)
|
|
|
+ console.log('handleParentMemberChange -> index', index)
|
|
|
this.selectParentMemberByIndex(index)
|
|
|
- this.loadHistoryMessages({ reset: true })
|
|
|
+ await this.loadHistoryMessages({ reset: true })
|
|
|
},
|
|
|
selectParentMemberByIndex(index = 0) {
|
|
|
const safeIndex = Math.max(0, Math.min(index, this.parentChatMembers.length - 1))
|
|
|
@@ -530,6 +714,7 @@ const miniprogramState = (() => {
|
|
|
const target = this.parentChatMembers[safeIndex] || {}
|
|
|
this.sessionContactId = String(target.ryid || '')
|
|
|
this.currentChatName = target.xm || '留言'
|
|
|
+ this.refreshCurrentUserMeta()
|
|
|
this.resolveWsIdentity()
|
|
|
this.currentWsConfig = this.buildWsConnectOptions()
|
|
|
},
|
|
|
@@ -547,9 +732,132 @@ const miniprogramState = (() => {
|
|
|
...this.parentInfo,
|
|
|
...info
|
|
|
}
|
|
|
+ this.refreshCurrentUserMeta()
|
|
|
+ },
|
|
|
+ getStoredUserInfo() {
|
|
|
+ let info = uni.getStorageSync('userInfo') || {}
|
|
|
+ if (typeof info === 'string') {
|
|
|
+ try {
|
|
|
+ info = JSON.parse(info)
|
|
|
+ } catch (error) {
|
|
|
+ info = {}
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return info && typeof info === 'object' ? info : {}
|
|
|
+ },
|
|
|
+ getSelfDisplayMeta() {
|
|
|
+ const userInfo = this.getStoredUserInfo()
|
|
|
+ if (this.sessionRole === 'device') {
|
|
|
+ return {
|
|
|
+ department: '学生',
|
|
|
+ name: userInfo.xm || this.currentUserMeta.name || '学生'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ department: '家长',
|
|
|
+ name: this.parentInfo?.xm || userInfo.xm || this.currentUserMeta.name || '家长'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ getPeerDisplayMeta() {
|
|
|
+ if (this.sessionRole === 'device') {
|
|
|
+ return {
|
|
|
+ department: '家长',
|
|
|
+ name: this.currentChatName || '家长'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ department: '学生',
|
|
|
+ name: this.currentChatName || '学生'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ resolveMessageMetaByDirection(direction = 'right') {
|
|
|
+ return direction === 'right' ? this.getSelfDisplayMeta() : this.getPeerDisplayMeta()
|
|
|
+ },
|
|
|
+ getSelfAvatarMeta() {
|
|
|
+ const userInfo = this.getStoredUserInfo()
|
|
|
+ const peerType = String(this.peerAvatarTypeHint || '')
|
|
|
+ const preferType = peerType === '1' ? '51' : (peerType === '51' ? '1' : '')
|
|
|
+ if (preferType === '51') {
|
|
|
+ return {
|
|
|
+ url: this.toDisplayImageUrl(userInfo.zjzwj || userInfo.yszwj || ''),
|
|
|
+ type: '51'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (preferType === '1') {
|
|
|
+ return {
|
|
|
+ url: this.toDisplayImageUrl(userInfo.yszwj || userInfo.zjzwj || ''),
|
|
|
+ type: '1'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (this.sessionRole === 'device') {
|
|
|
+ if (userInfo.zjzwj) {
|
|
|
+ return {
|
|
|
+ url: this.toDisplayImageUrl(userInfo.zjzwj),
|
|
|
+ type: '51'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (userInfo.yszwj) {
|
|
|
+ return {
|
|
|
+ url: this.toDisplayImageUrl(userInfo.yszwj),
|
|
|
+ type: '1'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ url: '/static/logo.png',
|
|
|
+ type: ''
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (userInfo.yszwj) {
|
|
|
+ return {
|
|
|
+ url: this.toDisplayImageUrl(userInfo.yszwj),
|
|
|
+ type: '1'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (userInfo.zjzwj) {
|
|
|
+ return {
|
|
|
+ url: this.toDisplayImageUrl(userInfo.zjzwj),
|
|
|
+ type: '51'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ url: '/static/logo.png',
|
|
|
+ type: ''
|
|
|
+ }
|
|
|
+ },
|
|
|
+ getAvatarSrc(message = {}) {
|
|
|
+ if (!message || message.direction === 'right') {
|
|
|
+ return this.getSelfAvatarMeta().url
|
|
|
+ }
|
|
|
+ return message.avatarUrl || '/static/logo.png'
|
|
|
+ },
|
|
|
+ getAvatarClass(message = {}) {
|
|
|
+ const type = String(
|
|
|
+ message && message.direction === 'right'
|
|
|
+ ? this.getSelfAvatarMeta().type
|
|
|
+ : (message.avatarType || '')
|
|
|
+ )
|
|
|
+ return {
|
|
|
+ 'square-avatar': type === '51',
|
|
|
+ 'doc-avatar': type === '51'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ refreshCurrentUserMeta() {
|
|
|
+ const selfMeta = this.getSelfDisplayMeta()
|
|
|
+ this.currentUserMeta = {
|
|
|
+ ...this.currentUserMeta,
|
|
|
+ department: selfMeta.department,
|
|
|
+ name: selfMeta.name
|
|
|
+ }
|
|
|
},
|
|
|
buildCallContactByCurrentMember() {
|
|
|
const member = this.parentChatMembers[this.parentChatMemberIndex] || {}
|
|
|
+ if (this.sessionRole === 'device') {
|
|
|
+ return {
|
|
|
+ username: member.xm || member.username || '家长',
|
|
|
+ avatar: member.yszwj ? this.toDisplayImageUrl(member.yszwj) : '/static/logo.png',
|
|
|
+ openid: member.wbid2 || member.wbid || ''
|
|
|
+ }
|
|
|
+ }
|
|
|
return {
|
|
|
username: member.xm || member.username || '学生',
|
|
|
avatar: member.yszwj ? this.toDisplayImageUrl(member.yszwj) : '/static/logo.png',
|
|
|
@@ -570,6 +878,57 @@ const miniprogramState = (() => {
|
|
|
uni.showToast({ title: '通话能力暂不可用', icon: 'none' })
|
|
|
return
|
|
|
}
|
|
|
+ if (this.sessionRole === 'device') {
|
|
|
+ const userInfo = this.getStoredUserInfo()
|
|
|
+ const callerId = String(userInfo.devId || this.currentWsConfig?.ssDevId || '').trim()
|
|
|
+ const listenerId = String(contact?.openid || '').trim()
|
|
|
+ if (!callerId) {
|
|
|
+ uni.showToast({ title: '缺少设备标识', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (!listenerId) {
|
|
|
+ uni.showToast({ title: '请让家长先关注公众号,登陆小程序后再发起', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ uni.showLoading({ title: '呼叫中...', mask: true })
|
|
|
+ uni.setStorageSync(CALL_END_STORAGE_KEY, {
|
|
|
+ name: contact?.username || '家长',
|
|
|
+ avatar: contact?.avatar || '/static/logo.png',
|
|
|
+ duration: 0,
|
|
|
+ time: new Date().toLocaleString(),
|
|
|
+ type: roomType,
|
|
|
+ status: '呼叫中'
|
|
|
+ })
|
|
|
+ this.setVoipEndPagePath()
|
|
|
+ try {
|
|
|
+ const res = await wmpfVoip.initByCaller({
|
|
|
+ roomType,
|
|
|
+ caller: {
|
|
|
+ id: callerId,
|
|
|
+ name: userInfo.xm || '学生'
|
|
|
+ },
|
|
|
+ listener: {
|
|
|
+ id: listenerId,
|
|
|
+ name: contact?.username || '家长'
|
|
|
+ },
|
|
|
+ businessType: 1,
|
|
|
+ miniprogramState
|
|
|
+ })
|
|
|
+ if (res.isSuccess) {
|
|
|
+ uni.hideLoading()
|
|
|
+ uni.redirectTo({ url: wmpfVoip.CALL_PAGE_PATH })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ uni.hideLoading()
|
|
|
+ uni.showToast({ title: '呼叫失败', icon: 'error' })
|
|
|
+ return
|
|
|
+ } catch (error) {
|
|
|
+ uni.hideLoading()
|
|
|
+ console.error('设备端通话异常:', error)
|
|
|
+ uni.showToast({ title: '发起通话失败', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
const callerId = this.parentInfo?.openid || this.parentInfo?.wbid || ''
|
|
|
if (!callerId) {
|
|
|
uni.showToast({ title: '缺少家长身份标识', icon: 'none' })
|
|
|
@@ -624,14 +983,91 @@ const miniprogramState = (() => {
|
|
|
uni.showToast({ title: '发起通话失败', icon: 'none' })
|
|
|
}
|
|
|
},
|
|
|
+ async handleDeviceLogout() {
|
|
|
+ try {
|
|
|
+ await deviceApi.ssExit()
|
|
|
+ } catch (error) {
|
|
|
+ console.warn('设备端退出服务调用失败:', error)
|
|
|
+ }
|
|
|
+ uni.removeStorageSync('userInfo')
|
|
|
+ uni.removeStorageSync('JSESSIONID')
|
|
|
+ uni.removeStorageSync('currcall')
|
|
|
+ uni.removeStorageSync('lastCallInfo')
|
|
|
+ if (this.inactivityTimer) {
|
|
|
+ clearTimeout(this.inactivityTimer)
|
|
|
+ this.inactivityTimer = null
|
|
|
+ }
|
|
|
+ uni.showToast({
|
|
|
+ title: '已退出',
|
|
|
+ icon: 'success',
|
|
|
+ duration: 1200
|
|
|
+ })
|
|
|
+ setTimeout(() => {
|
|
|
+ uni.reLaunch({ url: '/pages/device/notice' })
|
|
|
+ }, 1200)
|
|
|
+ },
|
|
|
+ resetInactivityTimer() {
|
|
|
+ if (this.sessionRole !== 'device') return
|
|
|
+ if (this.inactivityTimer) {
|
|
|
+ clearTimeout(this.inactivityTimer)
|
|
|
+ }
|
|
|
+ this.inactivityTimer = setTimeout(() => {
|
|
|
+ this.handleDeviceLogout()
|
|
|
+ }, 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
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ const result = await deviceApi.grfw_chkGrfw(code)
|
|
|
+ const data = result?.data || {}
|
|
|
+ if (data.ssCode === 0 && data.ssData) {
|
|
|
+ this.serviceStatus = data.ssData
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('检查服务状态失败:', error)
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async recordCallAndRefresh(callOptions = {}) {
|
|
|
+ if (this.sessionRole !== 'device') return
|
|
|
+ try {
|
|
|
+ const duration = parseInt(callOptions.duration, 10) || 0
|
|
|
+ const callType = callOptions.callType || 'voice'
|
|
|
+ if (duration <= 0) return
|
|
|
+ const minutes = Math.ceil(duration / 60)
|
|
|
+ const grfwxmm = callType === 'video' ? 115 : 111
|
|
|
+ await deviceApi.grfw_endGrfw({
|
|
|
+ grfwxmm,
|
|
|
+ sc: minutes,
|
|
|
+ ll: 0,
|
|
|
+ ms: `通话${duration}秒`
|
|
|
+ })
|
|
|
+ await this.checkServiceStatus(grfwxmm)
|
|
|
+ } catch (error) {
|
|
|
+ console.error('记录通话失败:', error)
|
|
|
+ }
|
|
|
+ },
|
|
|
setVoipEndPagePath(forceUpdate = false) {
|
|
|
if (this._voipEndPathSet && !forceUpdate) return
|
|
|
if (!wmpfVoip) return
|
|
|
const callInfo = uni.getStorageSync(CALL_END_STORAGE_KEY) || {}
|
|
|
- const query = [`role=parent`]
|
|
|
+ const query = [this.sessionRole === 'device' ? 'role=device' : 'role=parent']
|
|
|
if (this.sessionContactId) {
|
|
|
query.push(`contactId=${encodeURIComponent(this.sessionContactId)}`)
|
|
|
}
|
|
|
+ if (this.currentChatName) {
|
|
|
+ query.push(`contactName=${encodeURIComponent(this.currentChatName)}`)
|
|
|
+ }
|
|
|
+ if (this.sessionRole === 'device') {
|
|
|
+ const userInfo = this.getStoredUserInfo()
|
|
|
+ if (userInfo.devId) {
|
|
|
+ query.push(`sn=${encodeURIComponent(userInfo.devId)}`)
|
|
|
+ }
|
|
|
+ }
|
|
|
if (callInfo.name) {
|
|
|
query.push(`name=${encodeURIComponent(callInfo.name)}`)
|
|
|
query.push(`avatar=${encodeURIComponent(callInfo.avatar || '')}`)
|
|
|
@@ -659,6 +1095,9 @@ const miniprogramState = (() => {
|
|
|
if (hangupEvent.includes(eventName)) {
|
|
|
callInfo.duration = event.data?.keepTime || 0
|
|
|
callInfo.status = event.data?.keepTime > 0 ? '通话已结束' : '未接通'
|
|
|
+ if (callInfo.duration > 0) {
|
|
|
+ callInfo.needRecord = true
|
|
|
+ }
|
|
|
} else if (cancelEvent.includes(eventName)) {
|
|
|
callInfo.duration = 0
|
|
|
callInfo.status = '已取消'
|
|
|
@@ -675,13 +1114,44 @@ const miniprogramState = (() => {
|
|
|
uni.hideLoading()
|
|
|
uni.setStorageSync(CALL_END_STORAGE_KEY, callInfo)
|
|
|
this.setVoipEndPagePath(true)
|
|
|
+ if (this.sessionRole === 'device' && callInfo.needRecord && callInfo.duration > 0) {
|
|
|
+ this.recordCallAndRefresh({
|
|
|
+ duration: callInfo.duration,
|
|
|
+ callType: callInfo.type || 'voice'
|
|
|
+ })
|
|
|
+ delete callInfo.needRecord
|
|
|
+ uni.setStorageSync(CALL_END_STORAGE_KEY, callInfo)
|
|
|
+ }
|
|
|
+ this.resetInactivityTimer()
|
|
|
}
|
|
|
})
|
|
|
this._voipEventRegistered = true
|
|
|
},
|
|
|
formatPayloadTime(rawValue) {
|
|
|
if (!rawValue) return ''
|
|
|
- const parsed = new Date(rawValue)
|
|
|
+ const normalized = String(rawValue)
|
|
|
+ .replace(/\u202f/g, ' ')
|
|
|
+ .replace(/\u00a0/g, ' ')
|
|
|
+ .replace(/\s+/g, ' ')
|
|
|
+ .trim()
|
|
|
+ let parsed = new Date(normalized)
|
|
|
+ if (Number.isNaN(parsed.getTime())) {
|
|
|
+ const monthMap = {
|
|
|
+ Jan: 0, Feb: 1, Mar: 2, Apr: 3, May: 4, Jun: 5,
|
|
|
+ Jul: 6, Aug: 7, Sep: 8, Oct: 9, Nov: 10, Dec: 11
|
|
|
+ }
|
|
|
+ const match = normalized.match(/^([A-Za-z]{3})\s+(\d{1,2}),\s*(\d{4}),\s*(\d{1,2}):(\d{2}):(\d{2})\s*(AM|PM)$/i)
|
|
|
+ if (match) {
|
|
|
+ const mon = monthMap[match[1]]
|
|
|
+ let hh = Number(match[4])
|
|
|
+ const mm = Number(match[5])
|
|
|
+ const ss = Number(match[6])
|
|
|
+ const ap = String(match[7]).toUpperCase()
|
|
|
+ if (ap === 'PM' && hh < 12) hh += 12
|
|
|
+ if (ap === 'AM' && hh === 12) hh = 0
|
|
|
+ parsed = new Date(Number(match[3]), mon, Number(match[2]), hh, mm, ss)
|
|
|
+ }
|
|
|
+ }
|
|
|
if (Number.isNaN(parsed.getTime())) return ''
|
|
|
const hh = String(parsed.getHours()).padStart(2, '0')
|
|
|
const mm = String(parsed.getMinutes()).padStart(2, '0')
|
|
|
@@ -732,6 +1202,7 @@ const miniprogramState = (() => {
|
|
|
if (reset) {
|
|
|
this.messages = []
|
|
|
this.scrollIntoView = ''
|
|
|
+ this.peerAvatarTypeHint = ''
|
|
|
}
|
|
|
|
|
|
const config = this.currentWsConfig || this.buildWsConnectOptions()
|
|
|
@@ -792,7 +1263,7 @@ const miniprogramState = (() => {
|
|
|
role: this.sessionRole
|
|
|
}
|
|
|
if (this.sessionRole === 'device') {
|
|
|
- config.ssDev = String(userInfo.devId || '')
|
|
|
+ config.ssDevId = String(userInfo.devId || '')
|
|
|
config.heartbeat = true
|
|
|
config.autoReconnect = true
|
|
|
} else {
|
|
|
@@ -849,9 +1320,14 @@ const miniprogramState = (() => {
|
|
|
if (!this.currentWsConfig) {
|
|
|
this.initializeSessionFromOptions({ force: true })
|
|
|
}
|
|
|
- const config = this.currentWsConfig || {}
|
|
|
+ let config = this.currentWsConfig || {}
|
|
|
if (config.role === 'parent') return
|
|
|
- if (!config.ssDev) return
|
|
|
+ if (!config.ssDevId) {
|
|
|
+ this.resolveWsIdentity()
|
|
|
+ this.currentWsConfig = this.buildWsConnectOptions()
|
|
|
+ config = this.currentWsConfig || {}
|
|
|
+ }
|
|
|
+ if (!config.ssDevId) return
|
|
|
try {
|
|
|
await websocketService.ensureConnected(config)
|
|
|
} catch (error) {
|
|
|
@@ -897,12 +1373,20 @@ const miniprogramState = (() => {
|
|
|
const direction = String(payload.sendRyid || '') === String(this.wsIdentity.sendRyid || '')
|
|
|
? 'right'
|
|
|
: 'left'
|
|
|
+ const messageMeta = this.resolveMessageMetaByDirection(direction)
|
|
|
+ const payloadAlias = String(payload.alias || '').trim()
|
|
|
+ const payloadLogo = payload.logo ? this.toDisplayImageUrl(payload.logo) : ''
|
|
|
+ const payloadLogoType = String(payload.logoType || '')
|
|
|
+ if (direction === 'left' && (payloadLogoType === '1' || payloadLogoType === '51')) {
|
|
|
+ this.peerAvatarTypeHint = payloadLogoType
|
|
|
+ }
|
|
|
const msgId = payload.xxid || payload.msgId || ''
|
|
|
if (fromHistory && msgId) {
|
|
|
const exists = this.messages.some((item) => String(item.msgId || '') === String(msgId))
|
|
|
if (exists) return
|
|
|
}
|
|
|
const payloadTime = this.formatPayloadTime(payload.sendTime || payload.sendTimeStr || payload.time)
|
|
|
+ const displayTime = payloadTime || this.formatPayloadTime(new Date())
|
|
|
const receiptStatus = (payload.readTime || payload.rdTime || payload.readTm) ? 'read' : 'unread'
|
|
|
|
|
|
if (typeCode === '121') {
|
|
|
@@ -911,11 +1395,14 @@ const miniprogramState = (() => {
|
|
|
this.appendMessage({
|
|
|
type: 'file',
|
|
|
direction,
|
|
|
- department: direction === 'right' ? this.currentUserMeta.department : '对方',
|
|
|
- name: direction === 'right' ? this.currentUserMeta.name : '对方',
|
|
|
+ department: messageMeta.department,
|
|
|
+ name: messageMeta.name,
|
|
|
+ displayName: direction === 'left' ? (payloadAlias || messageMeta.name) : messageMeta.name,
|
|
|
+ avatarUrl: direction === 'left' ? payloadLogo : '',
|
|
|
+ avatarType: direction === 'left' ? payloadLogoType : '',
|
|
|
fileName: cont.baseName || this.getBaseNameFromPath(cont.fileName) || '未命名文件',
|
|
|
fileUrl: this.toDisplayFileUrl(cont.fileName || ''),
|
|
|
- time: payloadTime || undefined,
|
|
|
+ time: displayTime,
|
|
|
needReceipt: true,
|
|
|
receiptStatus,
|
|
|
msgId
|
|
|
@@ -924,12 +1411,15 @@ const miniprogramState = (() => {
|
|
|
}
|
|
|
this.appendMessage(createTextMessage({
|
|
|
direction,
|
|
|
- department: direction === 'right' ? this.currentUserMeta.department : '对方',
|
|
|
- name: direction === 'right' ? this.currentUserMeta.name : '对方',
|
|
|
+ department: messageMeta.department,
|
|
|
+ name: messageMeta.name,
|
|
|
+ displayName: direction === 'left' ? (payloadAlias || messageMeta.name) : messageMeta.name,
|
|
|
+ avatarUrl: direction === 'left' ? payloadLogo : '',
|
|
|
+ avatarType: direction === 'left' ? payloadLogoType : '',
|
|
|
content: cont.body || '',
|
|
|
needReceipt: true,
|
|
|
receiptStatus,
|
|
|
- time: payloadTime || undefined,
|
|
|
+ time: displayTime,
|
|
|
msgId
|
|
|
}))
|
|
|
return
|
|
|
@@ -938,10 +1428,13 @@ const miniprogramState = (() => {
|
|
|
if (typeCode === '122') {
|
|
|
this.appendMessage(createImageMessage({
|
|
|
direction,
|
|
|
- department: direction === 'right' ? this.currentUserMeta.department : '对方',
|
|
|
- name: direction === 'right' ? this.currentUserMeta.name : '对方',
|
|
|
+ department: messageMeta.department,
|
|
|
+ name: messageMeta.name,
|
|
|
+ displayName: direction === 'left' ? (payloadAlias || messageMeta.name) : messageMeta.name,
|
|
|
+ avatarUrl: direction === 'left' ? payloadLogo : '',
|
|
|
+ avatarType: direction === 'left' ? payloadLogoType : '',
|
|
|
imageUrl: this.toDisplayImageUrl(cont.fileName || cont.body || ''),
|
|
|
- time: payloadTime || undefined,
|
|
|
+ time: displayTime,
|
|
|
needReceipt: true,
|
|
|
receiptStatus,
|
|
|
msgId
|
|
|
@@ -953,15 +1446,18 @@ const miniprogramState = (() => {
|
|
|
this.appendMessage({
|
|
|
type: 'voice',
|
|
|
direction,
|
|
|
- department: direction === 'right' ? this.currentUserMeta.department : '对方',
|
|
|
- name: direction === 'right' ? this.currentUserMeta.name : '对方',
|
|
|
+ department: messageMeta.department,
|
|
|
+ name: messageMeta.name,
|
|
|
+ displayName: direction === 'left' ? (payloadAlias || messageMeta.name) : messageMeta.name,
|
|
|
+ avatarUrl: direction === 'left' ? payloadLogo : '',
|
|
|
+ avatarType: direction === 'left' ? payloadLogoType : '',
|
|
|
duration: String(cont.duration || ''),
|
|
|
audioUrl: this.toDisplayFileUrl(cont.fileName || '', 'aud'),
|
|
|
voicePreview: '',
|
|
|
voiceText: cont.body || '',
|
|
|
needReceipt: true,
|
|
|
receiptStatus,
|
|
|
- time: payloadTime || undefined,
|
|
|
+ time: displayTime,
|
|
|
msgId
|
|
|
})
|
|
|
return
|
|
|
@@ -971,12 +1467,15 @@ const miniprogramState = (() => {
|
|
|
this.appendMessage({
|
|
|
type: 'video',
|
|
|
direction,
|
|
|
- department: direction === 'right' ? this.currentUserMeta.department : '对方',
|
|
|
- name: direction === 'right' ? this.currentUserMeta.name : '对方',
|
|
|
+ department: messageMeta.department,
|
|
|
+ name: messageMeta.name,
|
|
|
+ displayName: direction === 'left' ? (payloadAlias || messageMeta.name) : messageMeta.name,
|
|
|
+ avatarUrl: direction === 'left' ? payloadLogo : '',
|
|
|
+ avatarType: direction === 'left' ? payloadLogoType : '',
|
|
|
coverUrl: '/static/logo.png',
|
|
|
videoUrl: this.toDisplayFileUrl(cont.fileName || '', 'vid'),
|
|
|
duration: String(cont.duration || ''),
|
|
|
- time: payloadTime || undefined,
|
|
|
+ time: displayTime,
|
|
|
needReceipt: true,
|
|
|
receiptStatus,
|
|
|
msgId
|
|
|
@@ -1026,14 +1525,15 @@ const miniprogramState = (() => {
|
|
|
this.$set(this.messages[index], 'receiptStatus', 'read')
|
|
|
},
|
|
|
async sendWsPayload(payload) {
|
|
|
+ this.resetInactivityTimer()
|
|
|
const config = this.currentWsConfig || this.buildWsConnectOptions()
|
|
|
if (!config) return false
|
|
|
if (config.role === 'parent' && !config.ssToken) {
|
|
|
uni.showToast({ title: '缺少 ssToken', icon: 'none' })
|
|
|
return false
|
|
|
}
|
|
|
- if (config.role === 'device' && !config.ssDev) {
|
|
|
- uni.showToast({ title: '缺少 ssDev', icon: 'none' })
|
|
|
+ if (config.role === 'device' && !config.ssDevId) {
|
|
|
+ uni.showToast({ title: '缺少 ssDevId', icon: 'none' })
|
|
|
return false
|
|
|
}
|
|
|
|
|
|
@@ -1426,6 +1926,7 @@ const miniprogramState = (() => {
|
|
|
this.draftText = `${this.draftText}${emoji}`
|
|
|
},
|
|
|
handleListTouchStart(event) {
|
|
|
+ this.resetInactivityTimer()
|
|
|
const touch = event.touches && event.touches[0]
|
|
|
if (!touch) return
|
|
|
this.listTouchMoved = false
|
|
|
@@ -1433,6 +1934,7 @@ const miniprogramState = (() => {
|
|
|
this.touchStartY = touch.clientY
|
|
|
},
|
|
|
handleListTouchMove(event) {
|
|
|
+ this.resetInactivityTimer()
|
|
|
const touch = event.touches && event.touches[0]
|
|
|
if (!touch) return
|
|
|
const deltaX = Math.abs(touch.clientX - this.touchStartX)
|
|
|
@@ -1442,6 +1944,7 @@ const miniprogramState = (() => {
|
|
|
}
|
|
|
},
|
|
|
handleListTouchEnd() {
|
|
|
+ this.resetInactivityTimer()
|
|
|
if (!this.listTouchMoved) {
|
|
|
this.handlePageClick()
|
|
|
}
|
|
|
@@ -1723,6 +2226,12 @@ const miniprogramState = (() => {
|
|
|
justify-content: center;
|
|
|
}
|
|
|
|
|
|
+ .chat-logout-icon {
|
|
|
+ width: 36rpx;
|
|
|
+ height: 36rpx;
|
|
|
+ display: block;
|
|
|
+ }
|
|
|
+
|
|
|
.message-list {
|
|
|
flex: 1;
|
|
|
padding: 20rpx;
|
|
|
@@ -1752,6 +2261,14 @@ const miniprogramState = (() => {
|
|
|
flex-shrink: 0;
|
|
|
}
|
|
|
|
|
|
+ .avatar.square-avatar {
|
|
|
+ border-radius: 8rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .avatar.doc-avatar {
|
|
|
+ object-position: center 5px;
|
|
|
+ }
|
|
|
+
|
|
|
.msg-time {
|
|
|
font-size: 32rpx;
|
|
|
color: #666;
|
|
|
@@ -1771,9 +2288,14 @@ const miniprogramState = (() => {
|
|
|
color: #666;
|
|
|
}
|
|
|
|
|
|
+ .user-info-role {
|
|
|
+ display: none;
|
|
|
+ }
|
|
|
+
|
|
|
.message-content {
|
|
|
display: flex;
|
|
|
- align-items: center;
|
|
|
+ align-items: flex-end;
|
|
|
+ gap: 10rpx;
|
|
|
}
|
|
|
|
|
|
/* 文字消息容器 */
|
|
|
@@ -1938,6 +2460,10 @@ const miniprogramState = (() => {
|
|
|
border-radius: 50%;
|
|
|
}
|
|
|
|
|
|
+ .right .avatar.square-avatar {
|
|
|
+ border-radius: 8rpx;
|
|
|
+ }
|
|
|
+
|
|
|
.right .content-section {
|
|
|
align-items: flex-end;
|
|
|
}
|
|
|
@@ -1946,6 +2472,15 @@ const miniprogramState = (() => {
|
|
|
text-align: right;
|
|
|
}
|
|
|
|
|
|
+ .user-info-placeholder {
|
|
|
+ color: transparent;
|
|
|
+ }
|
|
|
+
|
|
|
+ .user-info-placeholder .user-info-role {
|
|
|
+ display: inline;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
/* 右侧消息气泡样式 */
|
|
|
.right .text-message,
|
|
|
.right .call-message,
|