web会议聊天
This commit is contained in:
@@ -111,7 +111,7 @@ services:
|
||||
# 基础配置(局域网访问)
|
||||
TZ: Asia/Shanghai
|
||||
# 关键:使用 http:// 协议的完整 URL
|
||||
PUBLIC_URL: http://192.168.0.253:8280
|
||||
PUBLIC_URL: 192.168.0.253:8280
|
||||
|
||||
# 关键:禁用 HTTPS,让容器生成 ws:// 而不是 wss://
|
||||
ENABLE_HTTPS: 0
|
||||
@@ -139,7 +139,7 @@ services:
|
||||
ENABLE_GUESTS: 1
|
||||
AUTH_TYPE: jwt
|
||||
JWT_APP_ID: urbanLifeline
|
||||
JWT_APP_SECRET: urbanLifelinejitsi
|
||||
JWT_APP_SECRET: urbanLifeline-jitsi-secret-key-2025-production-safe-hs256
|
||||
JWT_ACCEPTED_ISSUERS: urbanLifeline
|
||||
JWT_ACCEPTED_AUDIENCES: jitsi
|
||||
JWT_ASAP_KEYSERVER: https://192.168.0.253:8280/
|
||||
@@ -206,7 +206,7 @@ services:
|
||||
ENABLE_GUESTS: 1
|
||||
AUTH_TYPE: jwt
|
||||
JWT_APP_ID: urbanLifeline
|
||||
JWT_APP_SECRET: urbanLifelinejitsi
|
||||
JWT_APP_SECRET: urbanLifeline-jitsi-secret-key-2025-production-safe-hs256
|
||||
JWT_ACCEPTED_ISSUERS: urbanLifeline
|
||||
JWT_ACCEPTED_AUDIENCES: jitsi
|
||||
JWT_ALLOW_EMPTY: 0
|
||||
|
||||
@@ -62,8 +62,14 @@ public class JitsiTokenServiceImpl implements JitsiTokenService {
|
||||
claims.put("exp", exp / 1000); // 秒级时间戳
|
||||
claims.put("nbf", now / 1000);
|
||||
|
||||
// 构建JWT Header,必须包含 typ: JWT
|
||||
Map<String, Object> header = new HashMap<>();
|
||||
header.put("alg", "HS256");
|
||||
header.put("typ", "JWT");
|
||||
|
||||
// 生成JWT Token
|
||||
String token = Jwts.builder()
|
||||
.setHeader(header)
|
||||
.setClaims(claims)
|
||||
.setIssuedAt(new Date(now))
|
||||
.setExpiration(new Date(exp))
|
||||
|
||||
@@ -105,7 +105,7 @@ jitsi:
|
||||
secret: urbanLifeline-jitsi-secret-key-2025-production-safe-hs256
|
||||
server:
|
||||
# Jitsi Meet服务器地址(Docker部署在本地8280端口)
|
||||
url: http://192.168.0.253:8280
|
||||
url: http://localhost:8280
|
||||
token:
|
||||
# JWT Token有效期(毫秒)- 默认2小时
|
||||
expiration: 7200000
|
||||
@@ -16,10 +16,14 @@
|
||||
v-if="finalUrl"
|
||||
ref="iframeRef"
|
||||
:src="finalUrl"
|
||||
:key="iframeKey"
|
||||
class="iframe-content"
|
||||
:class="{ 'with-header': showHeader }"
|
||||
frameborder="0"
|
||||
allow="camera; microphone; fullscreen; display-capture; autoplay"
|
||||
allowfullscreen
|
||||
@load="handleLoad"
|
||||
@error="handleError"
|
||||
/>
|
||||
<div v-else class="iframe-error">
|
||||
<AlertTriangle :size="48" class="error-icon" />
|
||||
@@ -33,7 +37,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { ref, computed, onMounted, watch, onUnmounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { Loader, AlertTriangle, RefreshCw } from 'lucide-vue-next'
|
||||
|
||||
@@ -41,21 +45,26 @@ interface Props {
|
||||
url?: string // 直接传入的 URL(优先级高于 route.meta)
|
||||
title?: string // 标题
|
||||
showHeader?: boolean // 是否显示头部(带刷新按钮)
|
||||
timeout?: number // 加载超时时间(毫秒),默认 10000
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
url: '',
|
||||
title: '',
|
||||
showHeader: false
|
||||
showHeader: false,
|
||||
timeout: 10000
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
load: []
|
||||
error: [error: string]
|
||||
}>()
|
||||
|
||||
const route = useRoute()
|
||||
const loading = ref(true)
|
||||
const iframeRef = ref<HTMLIFrameElement>()
|
||||
const iframeKey = ref(0)
|
||||
let loadTimeout: number | null = null
|
||||
|
||||
// 最终的 iframe URL(props.url 优先,否则从 route.meta 获取)
|
||||
const finalUrl = computed(() => {
|
||||
@@ -63,22 +72,54 @@ const finalUrl = computed(() => {
|
||||
})
|
||||
|
||||
function handleLoad() {
|
||||
clearLoadTimeout()
|
||||
loading.value = false
|
||||
console.log('[IframeView] iframe 加载完成')
|
||||
emit('load')
|
||||
}
|
||||
|
||||
function handleError(e: Event) {
|
||||
clearLoadTimeout()
|
||||
loading.value = false
|
||||
console.error('[IframeView] iframe 加载错误:', e)
|
||||
emit('error', 'iframe 加载失败')
|
||||
}
|
||||
|
||||
function clearLoadTimeout() {
|
||||
if (loadTimeout) {
|
||||
clearTimeout(loadTimeout)
|
||||
loadTimeout = null
|
||||
}
|
||||
}
|
||||
|
||||
function startLoadTimeout() {
|
||||
clearLoadTimeout()
|
||||
// 设置超时,如果超时后仍在加载,则隐藏加载状态
|
||||
// 因为某些情况下 iframe 的 load 事件可能不会触发
|
||||
loadTimeout = window.setTimeout(() => {
|
||||
if (loading.value) {
|
||||
console.warn('[IframeView] iframe 加载超时,隐藏加载状态')
|
||||
loading.value = false
|
||||
}
|
||||
}, props.timeout)
|
||||
}
|
||||
|
||||
// 刷新 iframe
|
||||
function refresh() {
|
||||
if (iframeRef.value) {
|
||||
loading.value = true
|
||||
iframeRef.value.src = iframeRef.value.src
|
||||
iframeKey.value++ // 通过改变 key 强制重新渲染 iframe
|
||||
startLoadTimeout()
|
||||
}
|
||||
}
|
||||
|
||||
// 监听 URL 变化,重新加载
|
||||
watch(finalUrl, () => {
|
||||
if (finalUrl.value) {
|
||||
watch(finalUrl, (newUrl) => {
|
||||
if (newUrl) {
|
||||
loading.value = true
|
||||
iframeKey.value++ // 强制重新渲染
|
||||
startLoadTimeout()
|
||||
console.log('[IframeView] URL 变化,重新加载:', newUrl)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -89,6 +130,13 @@ defineExpose({
|
||||
|
||||
onMounted(() => {
|
||||
console.log('[IframeView] 加载 iframe:', finalUrl.value)
|
||||
if (finalUrl.value) {
|
||||
startLoadTimeout()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clearLoadTimeout()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -157,7 +157,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="meeting-modal-body">
|
||||
<IframeView :src="meetingUrl" class="meeting-iframe" />
|
||||
<IframeView :url="meetingUrl" class="meeting-iframe" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user