temp jitsi
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
import { http } from '@/utils/http'
|
||||
|
||||
/**
|
||||
* 创建视频会议参数
|
||||
*/
|
||||
export interface CreateMeetingParams {
|
||||
roomId: string
|
||||
workcaseId?: string
|
||||
meetingName: string
|
||||
maxParticipants?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 视频会议VO
|
||||
*/
|
||||
export interface VideoMeetingVO {
|
||||
meetingId: string
|
||||
roomId: string
|
||||
workcaseId?: string
|
||||
meetingName: string
|
||||
meetingPassword?: string
|
||||
jwtToken: string
|
||||
jitsiRoomName: string
|
||||
jitsiServerUrl: string
|
||||
status: string
|
||||
creatorId: string
|
||||
creatorType: string
|
||||
creatorName: string
|
||||
participantCount: number
|
||||
maxParticipants: number
|
||||
actualStartTime?: string
|
||||
actualEndTime?: string
|
||||
durationSeconds?: number
|
||||
durationFormatted?: string
|
||||
iframeUrl: string
|
||||
config?: any
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建视频会议
|
||||
*/
|
||||
export const createVideoMeeting = (params: CreateMeetingParams) => {
|
||||
return http.post<VideoMeetingVO>('/workcase/chat/meeting/create', params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会议信息
|
||||
*/
|
||||
export const getMeetingInfo = (meetingId: string) => {
|
||||
return http.get<VideoMeetingVO>(`/workcase/chat/meeting/${meetingId}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取聊天室活跃会议
|
||||
*/
|
||||
export const getActiveMeeting = (roomId: string) => {
|
||||
return http.get<VideoMeetingVO>(`/workcase/chat/meeting/room/${roomId}/active`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 加入会议(生成用户专属JWT)
|
||||
*/
|
||||
export const joinMeeting = (meetingId: string) => {
|
||||
return http.post<VideoMeetingVO>(`/workcase/chat/meeting/${meetingId}/join`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始会议
|
||||
*/
|
||||
export const startVideoMeeting = (meetingId: string) => {
|
||||
return http.post(`/workcase/chat/meeting/${meetingId}/start`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束会议
|
||||
*/
|
||||
export const endVideoMeeting = (meetingId: string) => {
|
||||
return http.post<VideoMeetingVO>(`/workcase/chat/meeting/${meetingId}/end`)
|
||||
}
|
||||
@@ -47,18 +47,59 @@ $brand-color-hover: #004488;
|
||||
|
||||
// ==================== Jitsi Meet会议容器 ====================
|
||||
.meeting-container {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
height: 400px;
|
||||
background: #000;
|
||||
border-bottom: 2px solid $brand-color;
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #e5e7eb;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.meeting-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.close-meeting-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 6px 12px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.meeting-iframe {
|
||||
width: 100%;
|
||||
height: calc(100% - 48px);
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
// 按钮禁用状态
|
||||
.action-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
|
||||
&:hover {
|
||||
border-color: #e2e8f0;
|
||||
color: #64748b;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,14 @@
|
||||
|
||||
<!-- Jitsi Meet会议iframe -->
|
||||
<div v-if="showMeeting && meetingUrl" class="meeting-container">
|
||||
<IframeView :src="meetingUrl" />
|
||||
<div class="meeting-header">
|
||||
<span>视频会议进行中</span>
|
||||
<button class="close-meeting-btn" @click="handleEndMeeting">
|
||||
<X :size="20" />
|
||||
结束会议
|
||||
</button>
|
||||
</div>
|
||||
<IframeView :src="meetingUrl" class="meeting-iframe" />
|
||||
</div>
|
||||
|
||||
<!-- 聊天消息列表 -->
|
||||
@@ -72,12 +79,16 @@
|
||||
<footer class="input-area">
|
||||
<!-- 操作按钮区域 -->
|
||||
<div class="action-buttons">
|
||||
<!-- 发起会议按钮(始终显示) -->
|
||||
<button class="action-btn" @click="$emit('start-meeting')">
|
||||
<!-- 发起会议按钮 -->
|
||||
<button
|
||||
class="action-btn"
|
||||
:disabled="meetingLoading || showMeeting"
|
||||
@click="handleStartMeeting"
|
||||
>
|
||||
<Video :size="18" />
|
||||
发起会议
|
||||
{{ showMeeting ? '会议进行中' : '发起会议' }}
|
||||
</button>
|
||||
|
||||
|
||||
<!-- 额外的操作按钮插槽 -->
|
||||
<slot name="action-area"></slot>
|
||||
</div>
|
||||
@@ -125,17 +136,18 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick } from 'vue'
|
||||
import { FileText, Video, Paperclip, Send } from 'lucide-vue-next'
|
||||
import { ref, nextTick, onMounted } from 'vue'
|
||||
import { FileText, Video, Paperclip, Send, X } from 'lucide-vue-next'
|
||||
import IframeView from 'shared/components/iframe/IframeView.vue'
|
||||
import type { ChatRoomMessageVO } from '@/types/workcase'
|
||||
import { createVideoMeeting, getActiveMeeting, endVideoMeeting } from '@/api/workcase/meeting'
|
||||
|
||||
interface Props {
|
||||
messages: ChatRoomMessageVO[]
|
||||
currentUserId: string
|
||||
roomId: string
|
||||
roomName?: string
|
||||
meetingUrl?: string
|
||||
showMeeting?: boolean
|
||||
workcaseId?: string
|
||||
fileDownloadUrl?: string
|
||||
hasMore?: boolean
|
||||
loadingMore?: boolean
|
||||
@@ -143,7 +155,6 @@ interface Props {
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
roomName: '聊天室',
|
||||
showMeeting: false,
|
||||
fileDownloadUrl: '',
|
||||
hasMore: true,
|
||||
loadingMore: false
|
||||
@@ -153,11 +164,76 @@ const FILE_DOWNLOAD_URL = props.fileDownloadUrl
|
||||
|
||||
const emit = defineEmits<{
|
||||
'send-message': [content: string, files: File[]]
|
||||
'start-meeting': []
|
||||
'download-file': [fileId: string]
|
||||
'load-more': []
|
||||
}>()
|
||||
|
||||
// 会议相关状态
|
||||
const showMeeting = ref(false)
|
||||
const meetingUrl = ref('')
|
||||
const currentMeetingId = ref('')
|
||||
const meetingLoading = ref(false)
|
||||
|
||||
// 创建并加入会议
|
||||
const handleStartMeeting = async () => {
|
||||
try {
|
||||
meetingLoading.value = true
|
||||
|
||||
// 创建会议
|
||||
const createRes = await createVideoMeeting({
|
||||
roomId: props.roomId,
|
||||
workcaseId: props.workcaseId,
|
||||
meetingName: `工单 ${props.workcaseId || props.roomId} 技术支持`,
|
||||
maxParticipants: 10
|
||||
})
|
||||
|
||||
if (createRes.code === 0 && createRes.data) {
|
||||
currentMeetingId.value = createRes.data.meetingId
|
||||
meetingUrl.value = createRes.data.iframeUrl
|
||||
showMeeting.value = true
|
||||
} else {
|
||||
console.error('创建会议失败:', createRes.message)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建会议异常:', error)
|
||||
} finally {
|
||||
meetingLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 结束会议
|
||||
const handleEndMeeting = async () => {
|
||||
if (!currentMeetingId.value) return
|
||||
|
||||
try {
|
||||
await endVideoMeeting(currentMeetingId.value)
|
||||
showMeeting.value = false
|
||||
meetingUrl.value = ''
|
||||
currentMeetingId.value = ''
|
||||
} catch (error) {
|
||||
console.error('结束会议失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否有活跃会议
|
||||
const checkActiveMeeting = async () => {
|
||||
try {
|
||||
const res = await getActiveMeeting(props.roomId)
|
||||
if (res.code === 0 && res.data) {
|
||||
currentMeetingId.value = res.data.meetingId
|
||||
meetingUrl.value = res.data.iframeUrl
|
||||
showMeeting.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('无活跃会议')
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时检查是否有活跃会议
|
||||
onMounted(() => {
|
||||
checkActiveMeeting()
|
||||
})
|
||||
|
||||
// 滚动到顶部加载更多
|
||||
const handleScroll = (e: Event) => {
|
||||
const target = e.target as HTMLElement
|
||||
@@ -293,7 +369,9 @@ const renderMarkdown = (text: string): string => {
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
scrollToBottom
|
||||
scrollToBottom,
|
||||
handleStartMeeting,
|
||||
handleEndMeeting
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ export interface TbWorkcaseDTO extends BaseDTO {
|
||||
/** 工单ID */
|
||||
workcaseId?: string
|
||||
/** 聊天室ID */
|
||||
roomId: string
|
||||
roomId?: string
|
||||
/** 来客ID */
|
||||
userId?: string
|
||||
/** 来客姓名 */
|
||||
@@ -21,7 +21,7 @@ export interface TbWorkcaseDTO extends BaseDTO {
|
||||
/** 设备代码 */
|
||||
deviceCode?: string
|
||||
deviceNamePlate?: string
|
||||
deviceNamePlateImg: string
|
||||
deviceNamePlateImg?: string
|
||||
/** 地址 */
|
||||
address?: string
|
||||
/** 故障描述 */
|
||||
|
||||
@@ -148,6 +148,16 @@
|
||||
<el-button type="primary" @click="createTicket">创建</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 工单详情弹窗 -->
|
||||
<el-dialog v-model="showDetailDialog" title="工单详情" width="900px" destroy-on-close>
|
||||
<WorkcaseDetail
|
||||
v-if="showDetailDialog"
|
||||
:workcase="currentWorkcase"
|
||||
mode="view"
|
||||
@cancel="showDetailDialog = false"
|
||||
/>
|
||||
</el-dialog>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
@@ -157,8 +167,9 @@ import AdminLayout from '@/views/admin/AdminLayout.vue'
|
||||
import { Plus, Search } from 'lucide-vue-next'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { workcaseAPI } from '@/api/workcase'
|
||||
import WorkcaseDetail from '@/views/public/workcase/WorkcaseDetail/WorkcaseDetail.vue'
|
||||
import type { TbWorkcaseDTO, TbWorkcaseProcessDTO } from '@/types/workcase'
|
||||
import type { PageRequest, PageParam, ResultDomain } from 'shared/types'
|
||||
import type { PageRequest, PageParam } from 'shared/types'
|
||||
|
||||
const statusFilter = ref('all')
|
||||
const typeFilter = ref('')
|
||||
@@ -168,11 +179,11 @@ const currentPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const total = ref(0)
|
||||
const showCreateDialog = ref(false)
|
||||
const showDetailDialog = ref(false)
|
||||
const currentWorkcase = ref<TbWorkcaseDTO>({})
|
||||
const loading = ref(false)
|
||||
|
||||
const formData = ref<TbWorkcaseDTO>({
|
||||
roomId: '',
|
||||
deviceNamePlateImg: '',
|
||||
username: '',
|
||||
phone: '',
|
||||
device: '',
|
||||
@@ -215,7 +226,7 @@ const loadWorkcases = async () => {
|
||||
const res = await workcaseAPI.getWorkcasePage(pageRequest)
|
||||
if (res.success) {
|
||||
workcaseList.value = res.dataList || res.pageDomain?.dataList || []
|
||||
total.value = res.pageParam?.totalElements || 0
|
||||
total.value = res.pageDomain?.pageParam?.total || 0
|
||||
} else {
|
||||
ElMessage.error(res.message || '加载失败')
|
||||
}
|
||||
@@ -365,8 +376,8 @@ const handlePageChange = (page: number) => {
|
||||
}
|
||||
|
||||
const viewDetail = (row: TbWorkcaseDTO) => {
|
||||
ElMessage.info(`查看工单详情: ${row.workcaseId}`)
|
||||
// TODO: 跳转到工单详情页面
|
||||
currentWorkcase.value = { ...row }
|
||||
showDetailDialog.value = true
|
||||
}
|
||||
|
||||
const assignTicket = (row: TbWorkcaseDTO) => {
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
<div class="table-label">铭牌照片</div>
|
||||
<div class="table-value table-value-full">
|
||||
<div class="nameplate-photo" @click="previewNameplateImage">
|
||||
<img :src="formData.deviceNamePlateImg" alt="设备铭牌" />
|
||||
<img :src="getImageUrl(formData.deviceNamePlateImg)" alt="设备铭牌" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -132,7 +132,7 @@
|
||||
</div>
|
||||
<div class="photos-grid">
|
||||
<div v-for="(img, index) in formData.imgs" :key="index" class="photo-item">
|
||||
<img :src="img" alt="故障照片" />
|
||||
<img :src="getImageUrl(img)" alt="故障照片" />
|
||||
</div>
|
||||
<div v-if="mode !== 'view'" class="photo-upload">
|
||||
<Plus :size="32" />
|
||||
@@ -185,6 +185,7 @@ import { ChatMessage } from '@/views/public/ChatRoom/'
|
||||
import { ElButton, ElInput, ElSelect, ElOption, ElDialog, ElMessage } from 'element-plus'
|
||||
import { MessageSquare, ImageIcon as ImageIcon, Plus } from 'lucide-vue-next'
|
||||
import type { TbWorkcaseDTO } from '@/types/workcase/workcase'
|
||||
import { FILE_DOWNLOAD_URL } from '@/config'
|
||||
|
||||
interface TimelineItem {
|
||||
status: 'system' | 'manager' | 'engineer'
|
||||
@@ -225,6 +226,14 @@ const timeline = ref<TimelineItem[]>([
|
||||
}
|
||||
])
|
||||
|
||||
function getImageUrl(fileId: string): string {
|
||||
if (!fileId) return ''
|
||||
if (fileId.startsWith('http://') || fileId.startsWith('https://')) {
|
||||
return fileId
|
||||
}
|
||||
return `${FILE_DOWNLOAD_URL}${fileId}`
|
||||
}
|
||||
|
||||
// 根据 workcaseId 获取聊天室ID
|
||||
const loadChatRoom = async () => {
|
||||
if (!formData.value.workcaseId) return
|
||||
|
||||
@@ -214,5 +214,54 @@ export const workcaseChatAPI = {
|
||||
*/
|
||||
getWordCloudPage(pageRequest: PageRequest<TbWordCloudDTO>): Promise<ResultDomain<TbWordCloudDTO>> {
|
||||
return request<TbWordCloudDTO>({ url: `${this.baseUrl}/wordcloud/page`, method: 'POST', data: pageRequest })
|
||||
},
|
||||
|
||||
// ====================== 视频会议管理(Jitsi Meet) ======================
|
||||
|
||||
/**
|
||||
* 创建视频会议
|
||||
*/
|
||||
createVideoMeeting(params: {
|
||||
roomId: string
|
||||
workcaseId?: string
|
||||
meetingName: string
|
||||
maxParticipants?: number
|
||||
}): Promise<ResultDomain<any>> {
|
||||
return request({ url: `${this.baseUrl}/meeting/create`, method: 'POST', data: params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取会议信息
|
||||
*/
|
||||
getMeetingInfo(meetingId: string): Promise<ResultDomain<any>> {
|
||||
return request({ url: `${this.baseUrl}/meeting/${meetingId}`, method: 'GET' })
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取聊天室活跃会议
|
||||
*/
|
||||
getActiveMeeting(roomId: string): Promise<ResultDomain<any>> {
|
||||
return request({ url: `${this.baseUrl}/meeting/room/${roomId}/active`, method: 'GET' })
|
||||
},
|
||||
|
||||
/**
|
||||
* 加入会议(生成用户专属JWT)
|
||||
*/
|
||||
joinMeeting(meetingId: string): Promise<ResultDomain<any>> {
|
||||
return request({ url: `${this.baseUrl}/meeting/${meetingId}/join`, method: 'POST' })
|
||||
},
|
||||
|
||||
/**
|
||||
* 开始会议
|
||||
*/
|
||||
startVideoMeeting(meetingId: string): Promise<ResultDomain<any>> {
|
||||
return request({ url: `${this.baseUrl}/meeting/${meetingId}/start`, method: 'POST' })
|
||||
},
|
||||
|
||||
/**
|
||||
* 结束会议
|
||||
*/
|
||||
endVideoMeeting(meetingId: string): Promise<ResultDomain<any>> {
|
||||
return request({ url: `${this.baseUrl}/meeting/${meetingId}/end`, method: 'POST' })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/meeting/Meeting/Meeting",
|
||||
"path": "pages/meeting/Meeting",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
|
||||
@@ -532,10 +532,43 @@ function handleWorkcaseAction() {
|
||||
}
|
||||
|
||||
// 发起会议
|
||||
function startMeeting() {
|
||||
uni.navigateTo({
|
||||
url: `/pages/meeting/Meeting/Meeting?roomId=${roomId.value}&workcaseId=${workcaseId.value}`
|
||||
})
|
||||
async function startMeeting() {
|
||||
try {
|
||||
uni.showLoading({ title: '创建会议中...' })
|
||||
|
||||
// 调用后端API创建会议
|
||||
const res = await workcaseChatAPI.createVideoMeeting({
|
||||
roomId: roomId.value,
|
||||
workcaseId: workcaseId.value,
|
||||
meetingName: `工单 ${workcaseId.value || roomId.value} 技术支持`,
|
||||
maxParticipants: 10
|
||||
})
|
||||
|
||||
uni.hideLoading()
|
||||
|
||||
if (res.success && res.data) {
|
||||
const meetingUrl = res.data.iframeUrl
|
||||
const meetingId = res.data.meetingId
|
||||
|
||||
// 小程序/App使用webview打开会议
|
||||
uni.navigateTo({
|
||||
url: `/pages/meeting/MeetingView/MeetingView?meetingUrl=${encodeURIComponent(meetingUrl)}&meetingId=${meetingId}`,
|
||||
success: () => {
|
||||
console.log('[chatRoom] 跳转会议页面成功')
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('[chatRoom] 跳转会议页面失败:', err)
|
||||
uni.showToast({ title: '打开会议失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
} else {
|
||||
uni.showToast({ title: res.message || '创建会议失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
uni.hideLoading()
|
||||
console.error('[chatRoom] 创建会议失败:', e)
|
||||
uni.showToast({ title: '创建会议失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
.meeting-page {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.meeting-nav {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
z-index: 1000;
|
||||
|
||||
.nav-back {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.back-icon {
|
||||
color: #fff;
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.nav-right {
|
||||
.end-btn {
|
||||
color: #ff4444;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<view class="meeting-page">
|
||||
<!-- 自定义导航栏 -->
|
||||
<view class="meeting-nav" :style="{ paddingTop: statusBarHeight + 'px', height: navBarHeight + 'px' }">
|
||||
<view class="nav-back" @tap="confirmExit">
|
||||
<text class="back-icon">←</text>
|
||||
</view>
|
||||
<text class="nav-title">视频会议</text>
|
||||
<view class="nav-right" @tap="endMeeting">
|
||||
<text class="end-btn">结束会议</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Web-view加载Jitsi Meet -->
|
||||
<web-view
|
||||
:src="meetingUrl"
|
||||
:webview-styles="webviewStyles"
|
||||
@message="handleWebViewMessage"
|
||||
@error="handleWebViewError"
|
||||
></web-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { workcaseChatAPI } from '@/api/workcase'
|
||||
|
||||
const statusBarHeight = ref(44)
|
||||
const navBarHeight = ref(88)
|
||||
const meetingUrl = ref('')
|
||||
const meetingId = ref('')
|
||||
|
||||
const webviewStyles = ref({
|
||||
progress: {
|
||||
color: '#667eea'
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// 获取状态栏高度
|
||||
const windowInfo = uni.getWindowInfo()
|
||||
statusBarHeight.value = windowInfo.statusBarHeight || 44
|
||||
navBarHeight.value = statusBarHeight.value + 44
|
||||
|
||||
// 获取页面参数
|
||||
const pages = getCurrentPages()
|
||||
const currentPage = pages[pages.length - 1] as any
|
||||
if (currentPage && currentPage.options) {
|
||||
meetingUrl.value = decodeURIComponent(currentPage.options.meetingUrl || '')
|
||||
meetingId.value = currentPage.options.meetingId || ''
|
||||
}
|
||||
|
||||
console.log('[MeetingView] 会议页面加载:', {
|
||||
meetingId: meetingId.value,
|
||||
meetingUrl: meetingUrl.value
|
||||
})
|
||||
})
|
||||
|
||||
// 确认退出
|
||||
function confirmExit() {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要退出会议吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.navigateBack()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 结束会议
|
||||
async function endMeeting() {
|
||||
if (!meetingId.value) {
|
||||
uni.navigateBack()
|
||||
return
|
||||
}
|
||||
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要结束会议吗?这将关闭所有参与者的会议。',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
uni.showLoading({ title: '结束会议中...' })
|
||||
await workcaseChatAPI.endVideoMeeting(meetingId.value)
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '会议已结束', icon: 'success' })
|
||||
setTimeout(() => uni.navigateBack(), 1500)
|
||||
} catch (e) {
|
||||
uni.hideLoading()
|
||||
console.error('[MeetingView] 结束会议失败:', e)
|
||||
uni.showToast({ title: '结束会议失败', icon: 'none' })
|
||||
uni.navigateBack()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 处理webview消息
|
||||
function handleWebViewMessage(e: any) {
|
||||
console.log('[MeetingView] webview消息:', e)
|
||||
// 可以在这里处理Jitsi Meet发送的消息
|
||||
// 例如:会议结束、参与者加入/离开等事件
|
||||
}
|
||||
|
||||
// 处理webview错误
|
||||
function handleWebViewError(e: any) {
|
||||
console.error('[MeetingView] webview错误:', e)
|
||||
uni.showToast({ title: '会议加载失败', icon: 'none' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import url("./Meeting.scss")
|
||||
</style>
|
||||
@@ -1,184 +0,0 @@
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f4f5f7;
|
||||
}
|
||||
|
||||
.nav {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
padding-left: 24rpx;
|
||||
padding-right: 24rpx;
|
||||
padding-bottom: 16rpx;
|
||||
box-sizing: border-box;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.nav-back {
|
||||
width: 60rpx;
|
||||
height: 64rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.nav-back-icon {
|
||||
width: 20rpx;
|
||||
height: 20rpx;
|
||||
border-left: 4rpx solid #333;
|
||||
border-bottom: 4rpx solid #333;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
flex: 1;
|
||||
font-size: 34rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
line-height: 64rpx;
|
||||
}
|
||||
|
||||
.nav-capsule {
|
||||
width: 174rpx;
|
||||
height: 64rpx;
|
||||
}
|
||||
|
||||
.meeting-container {
|
||||
margin-top: 176rpx;
|
||||
padding: 48rpx 32rpx;
|
||||
min-height: calc(100vh - 176rpx);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.meeting-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80rpx 40rpx;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
.meeting-icon {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(145deg, #e8f7ff 0%, #c5e4ff 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 48rpx;
|
||||
box-shadow: 0 10rpx 40rpx rgba(180,220,255,0.5);
|
||||
}
|
||||
|
||||
.icon-text {
|
||||
font-size: 96rpx;
|
||||
}
|
||||
|
||||
.meeting-name {
|
||||
font-size: 44rpx;
|
||||
font-weight: 900;
|
||||
color: #1d72d3;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.meeting-desc {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
margin-bottom: 64rpx;
|
||||
}
|
||||
|
||||
.meeting-actions {
|
||||
margin-bottom: 80rpx;
|
||||
}
|
||||
|
||||
.join-btn {
|
||||
height: 96rpx;
|
||||
padding: 0 60rpx;
|
||||
background: linear-gradient(90deg, #173294 0%, #4a6fd9 100%);
|
||||
border-radius: 48rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.join-text {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.meeting-tips {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
padding: 32rpx;
|
||||
background: #f5f8ff;
|
||||
border-radius: 16rpx;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tip-item {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.in-meeting {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.meeting-webview {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.meeting-controls {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 48rpx;
|
||||
padding: 32rpx;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
padding: 24rpx 40rpx;
|
||||
background: #f5f8ff;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.control-btn.active {
|
||||
background: #fff7e6;
|
||||
}
|
||||
|
||||
.leave-btn {
|
||||
background: #fff1f0;
|
||||
}
|
||||
|
||||
.control-icon {
|
||||
font-size: 48rpx;
|
||||
}
|
||||
|
||||
.control-label {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
<template>
|
||||
<!-- #ifdef APP -->
|
||||
<scroll-view style="flex:1">
|
||||
<!-- #endif -->
|
||||
<view class="page">
|
||||
<!-- 自定义导航栏 -->
|
||||
<view class="nav" :style="{ paddingTop: headerPaddingTop + 'px', height: headerTotalHeight + 'px' }">
|
||||
<view class="nav-back" @tap="goBack">
|
||||
<view class="nav-back-icon"></view>
|
||||
</view>
|
||||
<text class="nav-title">视频会议</text>
|
||||
<view class="nav-capsule"></view>
|
||||
</view>
|
||||
|
||||
<!-- 会议内容区 -->
|
||||
<view class="meeting-container" :style="{ marginTop: headerTotalHeight + 'px' }">
|
||||
<!-- 会议信息 -->
|
||||
<view class="meeting-info" v-if="!isInMeeting">
|
||||
<view class="meeting-icon">
|
||||
<text class="icon-text">📹</text>
|
||||
</view>
|
||||
<text class="meeting-name">{{ meetingName || '视频会议' }}</text>
|
||||
<text class="meeting-desc">与客服进行实时视频沟通</text>
|
||||
|
||||
<view class="meeting-actions">
|
||||
<view class="join-btn" @tap="joinMeeting">
|
||||
<text class="join-text">加入会议</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="meeting-tips">
|
||||
<text class="tip-item">• 请确保网络连接稳定</text>
|
||||
<text class="tip-item">• 允许摄像头和麦克风权限</text>
|
||||
<text class="tip-item">• 建议在安静环境下进行会议</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 会议中状态 -->
|
||||
<view class="in-meeting" v-else>
|
||||
<!-- Jitsi Meet iframe 容器 -->
|
||||
<web-view v-if="iframeUrl" :src="iframeUrl" class="meeting-webview"></web-view>
|
||||
|
||||
<!-- 会议控制栏 -->
|
||||
<view class="meeting-controls">
|
||||
<view class="control-btn" :class="{ active: isMuted }" @tap="toggleMute">
|
||||
<text class="control-icon">{{ isMuted ? '🔇' : '🔊' }}</text>
|
||||
<text class="control-label">{{ isMuted ? '取消静音' : '静音' }}</text>
|
||||
</view>
|
||||
<view class="control-btn" :class="{ active: isVideoOff }" @tap="toggleVideo">
|
||||
<text class="control-icon">{{ isVideoOff ? '📷' : '📹' }}</text>
|
||||
<text class="control-label">{{ isVideoOff ? '开启视频' : '关闭视频' }}</text>
|
||||
</view>
|
||||
<view class="control-btn leave-btn" @tap="leaveMeeting">
|
||||
<text class="control-icon">📞</text>
|
||||
<text class="control-label">离开会议</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- #ifdef APP -->
|
||||
</scroll-view>
|
||||
<!-- #endif -->
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import { ref, onMounted } from 'vue'
|
||||
import type { VideoMeetingVO } from '@/types/workcase'
|
||||
|
||||
// 响应式数据
|
||||
const headerPaddingTop = ref<number>(44)
|
||||
const headerTotalHeight = ref<number>(88)
|
||||
const roomId = ref<string>('')
|
||||
const workcaseId = ref<string>('')
|
||||
const meetingName = ref<string>('视频会议')
|
||||
const isInMeeting = ref<boolean>(false)
|
||||
const iframeUrl = ref<string>('')
|
||||
const isMuted = ref<boolean>(false)
|
||||
const isVideoOff = ref<boolean>(false)
|
||||
|
||||
// 会议信息
|
||||
const meeting = ref<VideoMeetingVO>({})
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
uni.getSystemInfo({
|
||||
success: (res) => {
|
||||
// #ifdef MP-WEIXIN
|
||||
try {
|
||||
const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
|
||||
headerPaddingTop.value = menuButtonInfo.top
|
||||
headerTotalHeight.value = menuButtonInfo.bottom + 8
|
||||
} catch (e) {
|
||||
headerPaddingTop.value = res.statusBarHeight || 44
|
||||
headerTotalHeight.value = (res.statusBarHeight || 44) + 44
|
||||
}
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
headerPaddingTop.value = res.statusBarHeight || 44
|
||||
headerTotalHeight.value = (res.statusBarHeight || 44) + 44
|
||||
// #endif
|
||||
}
|
||||
})
|
||||
|
||||
// 获取页面参数
|
||||
const pages = getCurrentPages()
|
||||
const currentPage = pages[pages.length - 1] as any
|
||||
if (currentPage && currentPage.options) {
|
||||
roomId.value = currentPage.options.roomId || ''
|
||||
workcaseId.value = currentPage.options.workcaseId || ''
|
||||
}
|
||||
|
||||
loadMeetingInfo()
|
||||
})
|
||||
|
||||
// 加载会议信息
|
||||
function loadMeetingInfo() {
|
||||
console.log('加载会议信息:', roomId.value)
|
||||
// TODO: 调用 workcaseChatAPI 获取会议信息
|
||||
}
|
||||
|
||||
// 加入会议
|
||||
function joinMeeting() {
|
||||
uni.showLoading({ title: '正在加入会议...' })
|
||||
|
||||
// 模拟加入会议
|
||||
setTimeout(() => {
|
||||
uni.hideLoading()
|
||||
isInMeeting.value = true
|
||||
// TODO: 实际调用API创建/加入会议,获取iframeUrl
|
||||
// iframeUrl.value = meeting.value.iframeUrl || ''
|
||||
|
||||
uni.showToast({
|
||||
title: '已加入会议',
|
||||
icon: 'success'
|
||||
})
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// 离开会议
|
||||
function leaveMeeting() {
|
||||
uni.showModal({
|
||||
title: '离开会议',
|
||||
content: '确定要离开当前会议吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
isInMeeting.value = false
|
||||
iframeUrl.value = ''
|
||||
uni.showToast({
|
||||
title: '已离开会议',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 切换静音
|
||||
function toggleMute() {
|
||||
isMuted.value = !isMuted.value
|
||||
// TODO: 调用Jitsi API控制静音
|
||||
}
|
||||
|
||||
// 切换视频
|
||||
function toggleVideo() {
|
||||
isVideoOff.value = !isVideoOff.value
|
||||
// TODO: 调用Jitsi API控制视频
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
function goBack() {
|
||||
if (isInMeeting.value) {
|
||||
uni.showModal({
|
||||
title: '离开会议',
|
||||
content: '返回将离开当前会议,确定吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.navigateBack()
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
uni.navigateBack()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./Meeting.scss";
|
||||
</style>
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user