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