From 7402e72c08fbda6e41700be0508763f8d64db70a Mon Sep 17 00:00:00 2001 From: duoaohui <928970622@qq.com> Date: Sat, 23 May 2026 19:54:21 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A7=86=E9=A2=91=E4=BC=9A=E8=AE=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/cloud-law/video-call.vue | 168 +++++++++++++++++++- 1 file changed, 167 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/cloud-law/video-call.vue b/frontend/src/pages/cloud-law/video-call.vue index df24b88..77e2e00 100644 --- a/frontend/src/pages/cloud-law/video-call.vue +++ b/frontend/src/pages/cloud-law/video-call.vue @@ -1,5 +1,12 @@ @@ -128,9 +136,13 @@ const startedAt = ref(0) const nowTs = ref(Date.now()) const ended = ref(false) const preferSelfMain = ref(false) +const isDesktopWindow = ref(false) +const windowRect = ref({ left: 120, top: 80, width: 960, height: 540 }) let timerHandle = null let stateStopWatch = null let videoRetryHandle = null +let dragState = null +let resizeState = null const digitsOf = (value) => String(value || '').replace(/\D/g, '').slice(0, 9) const sameUser = (a, b) => String(a || '') === String(b || '') @@ -150,6 +162,15 @@ const speakingMap = computed(() => meetingStore.speakingMap || {}) const isVoiceMode = computed(() => callMode.value === 'voice') const cameraIcon = computed(() => videoEnabled.value ? videoOnIcon : videoOffIcon) const micIcon = computed(() => audioEnabled.value ? voiceOnIcon : voiceOffIcon) +const windowStyle = computed(() => { + if (!isDesktopWindow.value) return {} + return { + left: `${windowRect.value.left}px`, + top: `${windowRect.value.top}px`, + width: `${windowRect.value.width}px`, + height: `${windowRect.value.height}px` + } +}) const entryInitial = computed(() => { const name = userStore.userInfo?.nickname || userStore.userInfo?.username || '在' @@ -433,6 +454,80 @@ const swapPrimaryTile = () => { preferSelfMain.value = !preferSelfMain.value } +const clampWindowRect = (rect) => { + const viewportWidth = window.innerWidth || 1200 + const viewportHeight = window.innerHeight || 800 + const width = Math.min(Math.max(rect.width, 420), viewportWidth) + const height = Math.min(Math.max(rect.height, 260), viewportHeight) + return { + width, + height, + left: Math.min(Math.max(rect.left, 0), Math.max(0, viewportWidth - width)), + top: Math.min(Math.max(rect.top, 0), Math.max(0, viewportHeight - height)) + } +} + +const resetWindow = () => { + if (typeof window === 'undefined') return + const width = Math.min(960, Math.max(420, Math.floor(window.innerWidth * 0.82))) + const height = Math.min(540, Math.max(260, Math.floor(window.innerHeight * 0.72))) + windowRect.value = clampWindowRect({ + width, + height, + left: Math.floor((window.innerWidth - width) / 2), + top: Math.floor((window.innerHeight - height) / 2) + }) +} + +const startWindowDrag = (event) => { + if (!isDesktopWindow.value || event.button !== 0) return + dragState = { + startX: event.clientX, + startY: event.clientY, + rect: { ...windowRect.value } + } + window.addEventListener('mousemove', onWindowDrag) + window.addEventListener('mouseup', stopWindowMove) +} + +const onWindowDrag = (event) => { + if (!dragState) return + windowRect.value = clampWindowRect({ + ...dragState.rect, + left: dragState.rect.left + event.clientX - dragState.startX, + top: dragState.rect.top + event.clientY - dragState.startY + }) +} + +const startWindowResize = (event) => { + if (!isDesktopWindow.value || event.button !== 0) return + event.stopPropagation() + resizeState = { + startX: event.clientX, + startY: event.clientY, + rect: { ...windowRect.value } + } + window.addEventListener('mousemove', onWindowResize) + window.addEventListener('mouseup', stopWindowMove) +} + +const onWindowResize = (event) => { + if (!resizeState) return + windowRect.value = clampWindowRect({ + ...resizeState.rect, + width: resizeState.rect.width + event.clientX - resizeState.startX, + height: resizeState.rect.height + event.clientY - resizeState.startY + }) +} + +const stopWindowMove = () => { + dragState = null + resizeState = null + window.removeEventListener('mousemove', onWindowDrag) + window.removeEventListener('mousemove', onWindowResize) + window.removeEventListener('mouseup', stopWindowMove) +} + const onMicToggle = async () => { if (audioLoading.value || leaving.value) return audioLoading.value = true @@ -494,6 +589,10 @@ onLoad(query => { }) onMounted(() => { + if (typeof window !== 'undefined') { + isDesktopWindow.value = window.innerWidth >= 900 + if (isDesktopWindow.value) resetWindow() + } stateStopWatch = watch(() => meetingStore.localState, s => { if (s === MEETING_LOCAL_STATE_CONNECTED) { startTimer() @@ -521,6 +620,9 @@ onBeforeUnmount(() => { clearTimeout(videoRetryHandle) videoRetryHandle = null } + if (typeof window !== 'undefined') { + stopWindowMove() + } }) onUnload(() => { @@ -541,6 +643,70 @@ onUnload(() => { color: #ffffff; z-index: 1000; } +.cloud-law-call.desktop-window { + inset: auto; + border-radius: 10rpx; + box-shadow: 0 12rpx 48rpx rgba(0, 0, 0, 0.35); +} +.desktop-window .call-shell, +.desktop-window .entry-state { + height: calc(100% - 64rpx); + margin-top: 64rpx; +} +.window-titlebar { + position: absolute; + left: 0; + right: 0; + top: 0; + height: 64rpx; + padding: 0 24rpx; + display: flex; + align-items: center; + justify-content: space-between; + background: rgba(7, 16, 37, 0.96); + border-bottom: 1rpx solid rgba(255, 255, 255, 0.08); + box-sizing: border-box; + cursor: move; + z-index: 20; + user-select: none; +} +.window-title { + font-size: 26rpx; + font-weight: 600; + color: rgba(255, 255, 255, 0.92); +} +.window-actions { + display: flex; + align-items: center; + gap: 20rpx; +} +.window-action { + font-size: 24rpx; + color: rgba(255, 255, 255, 0.78); + cursor: pointer; +} +.window-action.danger { + color: #ff6b75; +} +.window-resizer { + position: absolute; + right: 0; + bottom: 0; + width: 36rpx; + height: 36rpx; + cursor: nwse-resize; + z-index: 30; +} +.window-resizer::after { + content: ''; + position: absolute; + right: 8rpx; + bottom: 8rpx; + width: 18rpx; + height: 18rpx; + border-right: 3rpx solid rgba(255, 255, 255, 0.55); + border-bottom: 3rpx solid rgba(255, 255, 255, 0.55); +} .entry-state { height: 100%; display: flex;