电话咨询

This commit is contained in:
duoaohui
2026-06-04 14:58:03 +08:00
parent b89b930c41
commit 3ffdaa30d9
4 changed files with 159 additions and 6 deletions

View File

@@ -49,6 +49,7 @@
</button>
<button
v-if="!replaceChatWithScreenShare"
class="btn btn-screen"
:class="{ active: screenSharing, loading: screenLoading }"
:disabled="screenLoading || screenDisabled"
@@ -127,7 +128,36 @@
<text class="label">成员</text>
</button>
<button class="btn" :disabled="!allowChat" @click="emitIf('chat')">
<button
v-if="replaceChatWithScreenShare"
class="btn btn-screen-share-slot"
:class="{ active: screenSharing, loading: screenLoading }"
:disabled="screenLoading || screenDisabled"
:title="screenDisabledReason || ''"
@click="emitIf('screen-toggle')"
>
<view class="icon">
<svg v-if="screenSharing" viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect>
<line x1="8" y1="21" x2="16" y2="21"></line>
<line x1="12" y1="17" x2="12" y2="21"></line>
<line x1="6" y1="7" x2="18" y2="13"></line>
<line x1="18" y1="7" x2="6" y2="13"></line>
</svg>
<svg v-else viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect>
<line x1="8" y1="21" x2="16" y2="21"></line>
<line x1="12" y1="17" x2="12" y2="21"></line>
<polyline points="9 10 12 7 15 10"></polyline>
<line x1="12" y1="7" x2="12" y2="14"></line>
</svg>
</view>
<text class="label">
<text class="label-desktop">{{ screenSharing ? '停止共享' : '共享屏幕' }}</text>
<text class="label-mobile">{{ screenSharing ? '停止' : '共享' }}</text>
</text>
</button>
<button v-else class="btn" :disabled="!allowChat" @click="emitIf('chat')">
<view class="icon">
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
@@ -173,7 +203,9 @@ const props = defineProps({
/** Phase B 录制:当前是否在录制中 */
recording: { type: Boolean, default: false },
/** Phase B 录制REST 调用 / 上传中 loading 态,禁用按钮 */
recordingLoading: { type: Boolean, default: false }
recordingLoading: { type: Boolean, default: false },
/** 云律律师端:用「共享屏幕」替换「聊天」按钮(移动端工具栏不隐藏屏幕共享) */
replaceChatWithScreenShare: { type: Boolean, default: false }
})
const emit = defineEmits(['mic-toggle', 'cam-toggle', 'screen-toggle', 'recording-toggle', 'invite', 'members', 'chat', 'leave'])
@@ -336,6 +368,10 @@ const emitIf = (name) => {
.btn-screen {
display: none;
}
/* 律师端:共享屏幕占原「聊天」槽位,移动端仍需显示 */
.btn-screen-share-slot {
display: flex;
}
.icon {
width: 64rpx;
height: 64rpx;

View File

@@ -96,7 +96,17 @@
</view>
<view class="controls-row controls-row-bottom">
<view class="control-item" @click="takeSnapshot" @tap="takeSnapshot">
<!-- 律师端底部左侧为共享屏幕替代通用会议页聊天能力 -->
<view v-if="isLawRole" class="control-item" @click="onScreenToggle" @tap="onScreenToggle">
<view
class="control-btn dark-btn text-icon-btn"
:class="{ active: screenSharingByMe, loading: screenLoading, disabled: screenDisabled && !screenSharingByMe }"
>
<text class="text-icon">{{ screenSharingByMe ? '停' : '屏' }}</text>
</view>
<text class="control-label">{{ screenSharingByMe ? '停止共享' : '共享屏幕' }}</text>
</view>
<view v-else class="control-item" @click="takeSnapshot" @tap="takeSnapshot">
<view class="control-btn dark-btn text-icon-btn">
<image v-if="!captureIconError" class="control-image" :src="captureIcon" mode="aspectFit" @error="captureIconError = true"></image>
<text v-else class="text-icon"></text>
@@ -177,6 +187,7 @@ const joining = ref(false)
const joinError = ref('')
const audioLoading = ref(false)
const videoLoading = ref(false)
const screenLoading = ref(false)
const leaving = ref(false)
const startedAt = ref(0)
const nowTs = ref(Date.now())
@@ -244,7 +255,56 @@ const participants = computed(() => {
const localTrackVersion = computed(() => {
const producers = meetingStore.localProducers || {}
return `${producers.audio || ''}-${producers.video || ''}`
return `${producers.audio || ''}-${producers.video || ''}-${producers.screen || ''}`
})
const screenSharingActive = computed(() => !!meetingStore.screenShare?.ownerUserId)
const screenSharingByMe = computed(() => {
const owner = meetingStore.screenShare?.ownerUserId
return !!owner && sameUser(owner, myUserId.value)
})
const screenSupported = computed(() => {
// #ifdef H5
return typeof navigator !== 'undefined' &&
!!navigator.mediaDevices &&
typeof navigator.mediaDevices.getDisplayMedia === 'function'
// #endif
// #ifndef H5
return false
// #endif
})
const screenDisabled = computed(() => {
if (!screenSupported.value) return true
if (screenSharingActive.value && !screenSharingByMe.value) return true
return false
})
const screenDisabledReason = computed(() => {
if (!screenSupported.value) return '当前环境不支持屏幕共享'
if (screenSharingActive.value && !screenSharingByMe.value) return '已有成员正在共享屏幕'
return ''
})
const screenTile = computed(() => {
const owner = meetingStore.screenShare?.ownerUserId
if (!owner) return null
localTrackVersion.value
remoteTrackVersion.value
const isLocal = sameUser(owner, myUserId.value)
const track = isLocal
? meetingStore.getLocalTrack('screen')
: meetingStore.getRemoteTrack(owner, 'screen')
if (!track) return null
return {
key: `screen_${owner}`,
userId: owner,
name: isLocal ? '你' : nameOf(owner),
isLocal,
isHost: sameUser(owner, hostId.value),
audioEnabled: false,
videoEnabled: true,
audioTrack: null,
videoTrack: track
}
})
const remoteTrackVersion = computed(() => {
@@ -252,7 +312,7 @@ const remoteTrackVersion = computed(() => {
const map = meetingStore.remoteConsumers || {}
Object.keys(map).forEach(uid => {
const slot = map[uid]
parts.push(`${uid}:${slot?.audio?.id || ''}:${slot?.video?.id || ''}`)
parts.push(`${uid}:${slot?.audio?.id || ''}:${slot?.video?.id || ''}:${slot?.screen?.id || ''}`)
})
return parts.join('|')
})
@@ -315,6 +375,7 @@ const tiles = computed(() => {
const selfTile = computed(() => tiles.value.find(t => t.isLocal) || null)
const remoteTile = computed(() => tiles.value.find(t => !t.isLocal && t.videoTrack) || tiles.value.find(t => !t.isLocal) || null)
const primaryTile = computed(() => {
if (screenTile.value) return screenTile.value
if (preferSelfMain.value && selfTile.value) return selfTile.value
return remoteTile.value || selfTile.value
})
@@ -650,6 +711,28 @@ const flipCamera = () => {
flipCameraByCommand()
}
const onScreenToggle = async () => {
if (screenLoading.value) return
if (screenDisabled.value && !screenSharingByMe.value) {
uni.showToast({ title: screenDisabledReason.value || '无法共享屏幕', icon: 'none' })
return
}
screenLoading.value = true
try {
if (screenSharingByMe.value) {
await meetingStore.stopScreenShare()
} else {
await meetingStore.startScreenShare()
}
} catch (err) {
if (err?.name !== 'NotAllowedError') {
uni.showToast({ title: err?.message || '屏幕共享失败', icon: 'none' })
}
} finally {
screenLoading.value = false
}
}
const executeCloudLawCommand = async (cmd = {}) => {
const command = String(cmd.command || cmd.type || '').toLowerCase()
const value = cmd.value
@@ -893,6 +976,13 @@ onLoad(query => {
cloudLawApiBase.value = query?.cloudLawApiBase || query?.cloud_law_api_base || ''
cloudLawToken.value = query?.cloudLawToken || query?.cloud_law_token || ''
cloudLawRole.value = query?.cloudLawRole || query?.cloud_law_role || ''
// #ifdef H5
if (String(cloudLawRole.value).toLowerCase() === 'law') {
try {
window.sessionStorage.setItem('echo_cloud_law_meeting_role', 'law')
} catch (e) {}
}
// #endif
const mode = String(query?.mode || query?.callType || query?.type || '').toLowerCase()
callMode.value = mode === 'voice' || mode === 'audio' ? 'voice' : 'video'
joinMeeting()
@@ -1233,6 +1323,10 @@ onUnload(() => {
.control-btn.disabled {
opacity: 0.94;
}
.control-btn.active {
background: linear-gradient(180deg, #4f8dff, #2563eb);
border-color: rgba(79, 141, 255, 0.55);
}
.hangup-btn {
width: 150rpx;
height: 150rpx;

View File

@@ -123,6 +123,7 @@ const mode = ref('create')
const joinCode = ref('')
const joinPassword = ref('')
const autoJoin = ref(false)
const cloudLawRole = ref('')
const initializing = ref(true)
// autoJoin 失败/超时的可见错误(替代无限"正在进入会议…"转圈)
const autoJoinError = ref('')
@@ -467,7 +468,8 @@ const onJoin = async (options = {}) => {
await meetingStore.joinAndEnter(joinCode.value, joinPassword.value, prefs)
}
const roomCode = meetingStore.currentRoom?.room_code
uni.redirectTo({ url: `/pages/meeting/room${roomCode ? `?code=${roomCode}` : ''}` })
const roleQ = cloudLawRole.value ? `&cloudLawRole=${encodeURIComponent(cloudLawRole.value)}` : ''
uni.redirectTo({ url: `/pages/meeting/room${roomCode ? `?code=${roomCode}${roleQ}` : (roleQ ? `?${roleQ.slice(1)}` : '')}` })
} catch (err) {
const msg = err?.message || JSON.stringify(err)
// autoJoin 模式下把错误显式呈现在页面toast 一闪而过,真机很难看清)
@@ -491,7 +493,15 @@ onLoad(async (query) => {
try {
mode.value = query?.mode === 'join' ? 'join' : 'create'
joinCode.value = query?.code || ''
cloudLawRole.value = query?.cloudLawRole || query?.cloud_law_role || ''
autoJoin.value = query?.autoJoin === '1' || query?.autoJoin === 'true' || query?.autoJoin === true
// #ifdef H5
if (String(cloudLawRole.value).toLowerCase() === 'law') {
try {
window.sessionStorage.setItem('echo_cloud_law_meeting_role', 'law')
} catch (e) {}
}
// #endif
console.log('[Preview] onLoad params:', {
query,
mode: mode.value,

View File

@@ -145,6 +145,7 @@
:show-recording="isHost"
:recording="isRecording"
:recording-loading="recordingLoading"
:replace-chat-with-screen-share="replaceChatWithScreenShare"
@mic-toggle="onMicToggle"
@cam-toggle="onCamToggle"
@screen-toggle="onScreenToggle"
@@ -230,6 +231,10 @@ import SelfVideoFloat from '@/components/meeting/SelfVideoFloat.vue'
const meetingStore = useMeetingStore()
const userStore = useUserStore()
/** 云律律师端:工具栏用共享屏幕替换聊天 */
const cloudLawRole = ref('')
const replaceChatWithScreenShare = computed(() => String(cloudLawRole.value).toLowerCase() === 'law')
const memberVisible = ref(false)
const inviteVisible = ref(false)
const chatVisible = ref(false)
@@ -839,6 +844,14 @@ let stateStopWatch = null
let redirectingToJoin = false
onLoad((query) => {
cloudLawRole.value = query?.cloudLawRole || query?.cloud_law_role || ''
if (!cloudLawRole.value) {
// #ifdef H5
try {
cloudLawRole.value = window.sessionStorage.getItem('echo_cloud_law_meeting_role') || ''
} catch (e) {}
// #endif
}
// 刷新页面 / 直链访问 room 时 store 为空,回跳 join 避免白屏
if (!meetingStore.isInMeeting) {
const paramCode = (query?.code || '').replace(/\D/g, '')