创建会议修正
This commit is contained in:
@@ -0,0 +1,122 @@
|
|||||||
|
package org.xyzh.workcase.config;
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Jitsi Meet 配置属性类
|
||||||
|
* @filename JitsiProperties.java
|
||||||
|
* @author claude
|
||||||
|
* @copyright xyzh
|
||||||
|
* @since 2025-12-27
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@ConfigurationProperties(prefix = "jitsi")
|
||||||
|
public class JitsiProperties {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用配置
|
||||||
|
*/
|
||||||
|
private App app = new App();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务器配置
|
||||||
|
*/
|
||||||
|
private Server server = new Server();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Token配置
|
||||||
|
*/
|
||||||
|
private Token token = new Token();
|
||||||
|
|
||||||
|
public App getApp() {
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setApp(App app) {
|
||||||
|
this.app = app;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Server getServer() {
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setServer(Server server) {
|
||||||
|
this.server = server;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Token getToken() {
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setToken(Token token) {
|
||||||
|
this.token = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用配置
|
||||||
|
*/
|
||||||
|
public static class App {
|
||||||
|
/**
|
||||||
|
* 应用ID(必须与Docker配置中的JWT_APP_ID一致)
|
||||||
|
*/
|
||||||
|
private String id = "urbanLifeline";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JWT密钥(必须与Docker配置中的JWT_APP_SECRET一致)
|
||||||
|
*/
|
||||||
|
private String secret = "your-secret-key-change-in-production";
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(String id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSecret() {
|
||||||
|
return secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSecret(String secret) {
|
||||||
|
this.secret = secret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务器配置
|
||||||
|
*/
|
||||||
|
public static class Server {
|
||||||
|
/**
|
||||||
|
* Jitsi Meet服务器地址
|
||||||
|
*/
|
||||||
|
private String url = "https://meet.jit.si";
|
||||||
|
|
||||||
|
public String getUrl() {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUrl(String url) {
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Token配置
|
||||||
|
*/
|
||||||
|
public static class Token {
|
||||||
|
/**
|
||||||
|
* JWT Token有效期(毫秒)- 默认2小时
|
||||||
|
*/
|
||||||
|
private Long expiration = 7200000L;
|
||||||
|
|
||||||
|
public Long getExpiration() {
|
||||||
|
return expiration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setExpiration(Long expiration) {
|
||||||
|
this.expiration = expiration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,8 +7,9 @@ import io.jsonwebtoken.SignatureAlgorithm;
|
|||||||
import org.apache.dubbo.config.annotation.DubboService;
|
import org.apache.dubbo.config.annotation.DubboService;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.xyzh.api.workcase.service.JitsiTokenService;
|
import org.xyzh.api.workcase.service.JitsiTokenService;
|
||||||
|
import org.xyzh.workcase.config.JitsiProperties;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -25,17 +26,8 @@ import java.util.Map;
|
|||||||
public class JitsiTokenServiceImpl implements JitsiTokenService {
|
public class JitsiTokenServiceImpl implements JitsiTokenService {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(JitsiTokenServiceImpl.class);
|
private static final Logger logger = LoggerFactory.getLogger(JitsiTokenServiceImpl.class);
|
||||||
|
|
||||||
@Value("${jitsi.app.id:urbanLifeline}")
|
@Autowired
|
||||||
private String jitsiAppId;
|
private JitsiProperties jitsiProperties;
|
||||||
|
|
||||||
@Value("${jitsi.app.secret:your-secret-key-change-in-production}")
|
|
||||||
private String jitsiAppSecret;
|
|
||||||
|
|
||||||
@Value("${jitsi.server.url:https://meet.jit.si}")
|
|
||||||
private String jitsiServerUrl;
|
|
||||||
|
|
||||||
@Value("${jitsi.token.expiration:7200000}")
|
|
||||||
private Long tokenExpiration; // 默认2小时
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String generateJwtToken(String roomName, String userId, String userName, boolean isModerator) {
|
public String generateJwtToken(String roomName, String userId, String userName, boolean isModerator) {
|
||||||
@@ -44,7 +36,7 @@ public class JitsiTokenServiceImpl implements JitsiTokenService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
long exp = now + tokenExpiration;
|
long exp = now + jitsiProperties.getToken().getExpiration();
|
||||||
|
|
||||||
// 构建用户上下文
|
// 构建用户上下文
|
||||||
Map<String, Object> userContext = new HashMap<>();
|
Map<String, Object> userContext = new HashMap<>();
|
||||||
@@ -56,9 +48,9 @@ public class JitsiTokenServiceImpl implements JitsiTokenService {
|
|||||||
Map<String, Object> claims = new HashMap<>();
|
Map<String, Object> claims = new HashMap<>();
|
||||||
claims.put("context", Map.of("user", userContext));
|
claims.put("context", Map.of("user", userContext));
|
||||||
claims.put("room", roomName);
|
claims.put("room", roomName);
|
||||||
claims.put("iss", jitsiAppId);
|
claims.put("iss", jitsiProperties.getApp().getId());
|
||||||
claims.put("aud", "jitsi");
|
claims.put("aud", "jitsi");
|
||||||
claims.put("sub", jitsiServerUrl);
|
claims.put("sub", jitsiProperties.getServer().getUrl());
|
||||||
claims.put("exp", exp / 1000); // 秒级时间戳
|
claims.put("exp", exp / 1000); // 秒级时间戳
|
||||||
claims.put("nbf", now / 1000);
|
claims.put("nbf", now / 1000);
|
||||||
|
|
||||||
@@ -73,7 +65,7 @@ public class JitsiTokenServiceImpl implements JitsiTokenService {
|
|||||||
.setClaims(claims)
|
.setClaims(claims)
|
||||||
.setIssuedAt(new Date(now))
|
.setIssuedAt(new Date(now))
|
||||||
.setExpiration(new Date(exp))
|
.setExpiration(new Date(exp))
|
||||||
.signWith(SignatureAlgorithm.HS256, jitsiAppSecret.getBytes())
|
.signWith(SignatureAlgorithm.HS256, jitsiProperties.getApp().getSecret().getBytes())
|
||||||
.compact();
|
.compact();
|
||||||
|
|
||||||
logger.info("JWT Token生成成功: roomName={}", roomName);
|
logger.info("JWT Token生成成功: roomName={}", roomName);
|
||||||
@@ -88,7 +80,7 @@ public class JitsiTokenServiceImpl implements JitsiTokenService {
|
|||||||
public boolean validateJwtToken(String token) {
|
public boolean validateJwtToken(String token) {
|
||||||
try {
|
try {
|
||||||
Claims claims = Jwts.parser()
|
Claims claims = Jwts.parser()
|
||||||
.setSigningKey(jitsiAppSecret.getBytes())
|
.setSigningKey(jitsiProperties.getApp().getSecret().getBytes())
|
||||||
.parseClaimsJws(token)
|
.parseClaimsJws(token)
|
||||||
.getBody();
|
.getBody();
|
||||||
|
|
||||||
@@ -106,7 +98,7 @@ public class JitsiTokenServiceImpl implements JitsiTokenService {
|
|||||||
logger.info("构建Jitsi iframe URL: roomName={}", roomName);
|
logger.info("构建Jitsi iframe URL: roomName={}", roomName);
|
||||||
|
|
||||||
StringBuilder url = new StringBuilder();
|
StringBuilder url = new StringBuilder();
|
||||||
url.append(jitsiServerUrl).append("/").append(roomName);
|
url.append(jitsiProperties.getServer().getUrl()).append("/").append(roomName);
|
||||||
|
|
||||||
// 添加JWT Token
|
// 添加JWT Token
|
||||||
url.append("?jwt=").append(jwtToken);
|
url.append("?jwt=").append(jwtToken);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import org.xyzh.common.auth.utils.LoginUtil;
|
|||||||
import org.xyzh.common.core.domain.LoginDomain;
|
import org.xyzh.common.core.domain.LoginDomain;
|
||||||
import org.xyzh.common.core.domain.ResultDomain;
|
import org.xyzh.common.core.domain.ResultDomain;
|
||||||
import org.xyzh.common.utils.id.IdUtil;
|
import org.xyzh.common.utils.id.IdUtil;
|
||||||
|
import org.xyzh.workcase.config.JitsiProperties;
|
||||||
import org.xyzh.workcase.mapper.TbChatRoomMemberMapper;
|
import org.xyzh.workcase.mapper.TbChatRoomMemberMapper;
|
||||||
import org.xyzh.workcase.mapper.TbVideoMeetingMapper;
|
import org.xyzh.workcase.mapper.TbVideoMeetingMapper;
|
||||||
|
|
||||||
@@ -53,6 +54,9 @@ public class VideoMeetingServiceImpl implements VideoMeetingService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private ChatRoomService chatRoomService;
|
private ChatRoomService chatRoomService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private JitsiProperties jitsiProperties;
|
||||||
|
|
||||||
@DubboReference(version = "1.0.0", group = "auth", timeout = 30000, retries = 0)
|
@DubboReference(version = "1.0.0", group = "auth", timeout = 30000, retries = 0)
|
||||||
private AuthService authService;
|
private AuthService authService;
|
||||||
|
|
||||||
@@ -132,7 +136,7 @@ public class VideoMeetingServiceImpl implements VideoMeetingService {
|
|||||||
if (meetingDTO.getAdvance() == null) {
|
if (meetingDTO.getAdvance() == null) {
|
||||||
meetingDTO.setAdvance(5); // 默认提前5分钟可入会
|
meetingDTO.setAdvance(5); // 默认提前5分钟可入会
|
||||||
}
|
}
|
||||||
|
meetingDTO.setJitsiServerUrl(jitsiProperties.getServer().getUrl());
|
||||||
// 6. 插入数据库
|
// 6. 插入数据库
|
||||||
int rows = videoMeetingMapper.insertVideoMeeting(meetingDTO);
|
int rows = videoMeetingMapper.insertVideoMeeting(meetingDTO);
|
||||||
if (rows > 0) {
|
if (rows > 0) {
|
||||||
|
|||||||
@@ -683,8 +683,9 @@ const subscribeToRoom = (roomId: string) => {
|
|||||||
|
|
||||||
roomSubscription = stompClient.subscribe(`/topic/chat/${roomId}`, (message: any) => {
|
roomSubscription = stompClient.subscribe(`/topic/chat/${roomId}`, (message: any) => {
|
||||||
const chatMessage = JSON.parse(message.body) as ChatRoomMessageVO
|
const chatMessage = JSON.parse(message.body) as ChatRoomMessageVO
|
||||||
// 避免重复添加自己发送的消息
|
// 避免重复添加自己发送的普通消息
|
||||||
if (chatMessage.senderId !== loginDomain.user.userId) {
|
// 但会议消息(meet类型)始终添加,因为它是系统生成的通知
|
||||||
|
if (chatMessage.messageType === 'meet' || chatMessage.senderId !== loginDomain.user.userId) {
|
||||||
messages.value.push(chatMessage)
|
messages.value.push(chatMessage)
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- 消息会议卡片 -->
|
<!-- 消息会议卡片 -->
|
||||||
<div class="meeting-card" :class="`meeting-card--${meeting.status}`">
|
<div v-if="loading" class="meeting-card meeting-card--loading">
|
||||||
|
<div class="meeting-card-loading">加载中...</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="meeting" class="meeting-card" :class="`meeting-card--${meeting.status}`">
|
||||||
<div class="meeting-card-header">
|
<div class="meeting-card-header">
|
||||||
<div class="meeting-card-title">{{ meeting.meetingName }}</div>
|
<div class="meeting-card-title">{{ meeting.meetingName }}</div>
|
||||||
<div class="meeting-card-status">
|
<div class="meeting-card-status">
|
||||||
@@ -28,15 +31,19 @@
|
|||||||
</ElButton>
|
</ElButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else class="meeting-card meeting-card--error">
|
||||||
|
<div class="meeting-card-error">会议信息加载失败</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ElButton, ElMessage } from 'element-plus'
|
import { ElButton, ElMessage } from 'element-plus'
|
||||||
import type { VideoMeetingVO } from '@/types'
|
import type { VideoMeetingVO } from '@/types'
|
||||||
import { computed, ref, onMounted, onUnmounted } from 'vue'
|
import { computed, ref, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { workcaseChatAPI } from '@/api/workcase'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
meeting: VideoMeetingVO
|
meetingId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
@@ -44,20 +51,56 @@ const emit = defineEmits<{
|
|||||||
join: [meetingId: string]
|
join: [meetingId: string]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
// 会议详情(从API获取的实时数据)
|
||||||
|
const meeting = ref<VideoMeetingVO | null>(null)
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
// 当前时间,每秒更新
|
// 当前时间,每秒更新
|
||||||
const currentTime = ref(Date.now())
|
const currentTime = ref(Date.now())
|
||||||
let timer: number | null = null
|
let timeTimer: number | null = null
|
||||||
|
let refreshTimer: number | null = null
|
||||||
|
|
||||||
|
// 获取会议详情
|
||||||
|
async function fetchMeetingInfo() {
|
||||||
|
if (!props.meetingId) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const result = await workcaseChatAPI.getVideoMeetingInfo(props.meetingId)
|
||||||
|
if (result.success && result.data) {
|
||||||
|
meeting.value = result.data
|
||||||
|
console.log('[MeetingCard] 获取会议信息成功:', result.data)
|
||||||
|
} else {
|
||||||
|
console.error('[MeetingCard] 获取会议信息失败:', result.message)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[MeetingCard] 获取会议信息异常:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
// 初始加载会议信息
|
||||||
|
fetchMeetingInfo()
|
||||||
|
|
||||||
// 每秒更新当前时间
|
// 每秒更新当前时间
|
||||||
timer = window.setInterval(() => {
|
timeTimer = window.setInterval(() => {
|
||||||
currentTime.value = Date.now()
|
currentTime.value = Date.now()
|
||||||
}, 1000)
|
}, 1000)
|
||||||
|
|
||||||
|
// 每30秒刷新会议状态
|
||||||
|
refreshTimer = window.setInterval(() => {
|
||||||
|
fetchMeetingInfo()
|
||||||
|
}, 30000)
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (timer) {
|
if (timeTimer) {
|
||||||
clearInterval(timer)
|
clearInterval(timeTimer)
|
||||||
|
}
|
||||||
|
if (refreshTimer) {
|
||||||
|
clearInterval(refreshTimer)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -78,15 +121,14 @@ function formatDateTime(dateStr?: string): string {
|
|||||||
* 计算倒计时文本
|
* 计算倒计时文本
|
||||||
*/
|
*/
|
||||||
const countdownText = computed(() => {
|
const countdownText = computed(() => {
|
||||||
const { meeting } = props
|
if (!meeting.value || !meeting.value.startTime || !meeting.value.endTime) {
|
||||||
if (!meeting || !meeting.startTime || !meeting.endTime) {
|
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const advanceMinutes = meeting.advance || 0
|
const advanceMinutes = meeting.value.advance || 0
|
||||||
const now = currentTime.value
|
const now = currentTime.value
|
||||||
const startTime = new Date(meeting.startTime).getTime()
|
const startTime = new Date(meeting.value.startTime).getTime()
|
||||||
const endTime = new Date(meeting.endTime).getTime()
|
const endTime = new Date(meeting.value.endTime).getTime()
|
||||||
|
|
||||||
// 检查时间解析是否有效
|
// 检查时间解析是否有效
|
||||||
if (isNaN(startTime) || isNaN(endTime)) {
|
if (isNaN(startTime) || isNaN(endTime)) {
|
||||||
@@ -95,7 +137,7 @@ const countdownText = computed(() => {
|
|||||||
|
|
||||||
const advanceTime = startTime - advanceMinutes * 60 * 1000
|
const advanceTime = startTime - advanceMinutes * 60 * 1000
|
||||||
|
|
||||||
if (meeting.status === 'ended') {
|
if (meeting.value.status === 'ended') {
|
||||||
return '会议已结束'
|
return '会议已结束'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +161,7 @@ const countdownText = computed(() => {
|
|||||||
return '可以入会'
|
return '可以入会'
|
||||||
} else if (now < endTime) {
|
} else if (now < endTime) {
|
||||||
// 会议进行中
|
// 会议进行中
|
||||||
if (meeting.status === 'ongoing') {
|
if (meeting.value.status === 'ongoing') {
|
||||||
return '会议进行中'
|
return '会议进行中'
|
||||||
} else {
|
} else {
|
||||||
return '可以入会'
|
return '可以入会'
|
||||||
@@ -134,19 +176,18 @@ const countdownText = computed(() => {
|
|||||||
* 是否可以加入会议
|
* 是否可以加入会议
|
||||||
*/
|
*/
|
||||||
const canJoinMeeting = computed(() => {
|
const canJoinMeeting = computed(() => {
|
||||||
const { meeting } = props
|
if (!meeting.value || !meeting.value.startTime || !meeting.value.endTime) {
|
||||||
if (!meeting || !meeting.startTime || !meeting.endTime) {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (meeting.status === 'ended') {
|
if (meeting.value.status === 'ended') {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const advanceMinutes = meeting.advance || 0
|
const advanceMinutes = meeting.value.advance || 0
|
||||||
const now = currentTime.value
|
const now = currentTime.value
|
||||||
const startTime = new Date(meeting.startTime).getTime()
|
const startTime = new Date(meeting.value.startTime).getTime()
|
||||||
const endTime = new Date(meeting.endTime).getTime()
|
const endTime = new Date(meeting.value.endTime).getTime()
|
||||||
|
|
||||||
// 检查时间解析是否有效
|
// 检查时间解析是否有效
|
||||||
if (isNaN(startTime) || isNaN(endTime)) {
|
if (isNaN(startTime) || isNaN(endTime)) {
|
||||||
@@ -163,8 +204,10 @@ const canJoinMeeting = computed(() => {
|
|||||||
* 按钮文本
|
* 按钮文本
|
||||||
*/
|
*/
|
||||||
const buttonText = computed(() => {
|
const buttonText = computed(() => {
|
||||||
const { meeting } = props
|
if (!meeting.value) {
|
||||||
if (meeting.status === 'ended') {
|
return '加载中'
|
||||||
|
}
|
||||||
|
if (meeting.value.status === 'ended') {
|
||||||
return '会议已结束'
|
return '会议已结束'
|
||||||
}
|
}
|
||||||
if (!canJoinMeeting.value) {
|
if (!canJoinMeeting.value) {
|
||||||
@@ -177,7 +220,7 @@ const buttonText = computed(() => {
|
|||||||
* 加入会议
|
* 加入会议
|
||||||
*/
|
*/
|
||||||
async function handleJoinMeeting() {
|
async function handleJoinMeeting() {
|
||||||
if (!props.meeting.meetingId) {
|
if (!props.meetingId) {
|
||||||
ElMessage.error('会议ID不存在')
|
ElMessage.error('会议ID不存在')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -187,9 +230,9 @@ async function handleJoinMeeting() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 发出事件让父组件处理
|
// 发出事件让父组件处理
|
||||||
emit('join', props.meeting.meetingId)
|
emit('join', props.meetingId)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@import './MeetingCard.scss';
|
@import url('./MeetingCard.scss');
|
||||||
</style>
|
</style>
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
<div class="message-content-wrapper">
|
<div class="message-content-wrapper">
|
||||||
<!-- 会议消息卡片 -->
|
<!-- 会议消息卡片 -->
|
||||||
<template v-if="message.messageType === 'meet'">
|
<template v-if="message.messageType === 'meet'">
|
||||||
<MeetingCard :meeting="getMeetingData(message.contentExtra)" @join="handleJoinMeeting" />
|
<MeetingCard :meetingId="getMeetingId(message.contentExtra)" @join="handleJoinMeeting" />
|
||||||
<div class="message-time">{{ formatTime(message.sendTime) }}</div>
|
<div class="message-time">{{ formatTime(message.sendTime) }}</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -186,7 +186,9 @@ const showMeetingCreate = ref(false)
|
|||||||
// 打开创建会议对话框或直接emit事件给父组件处理
|
// 打开创建会议对话框或直接emit事件给父组件处理
|
||||||
const handleStartMeeting = () => {
|
const handleStartMeeting = () => {
|
||||||
// emit事件给父组件,让父组件处理会议逻辑
|
// emit事件给父组件,让父组件处理会议逻辑
|
||||||
emit('start-meeting')
|
// emit('start-meeting')
|
||||||
|
showMeetingCreate.value = true
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 会议创建成功回调
|
// 会议创建成功回调
|
||||||
@@ -322,11 +324,13 @@ const handleJoinMeeting = async (meetingId: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取会议数据(将contentExtra转换为VideoMeetingVO)
|
// 获取会议数据(将contentExtra转换为VideoMeetingVO)
|
||||||
function getMeetingData(contentExtra: Record<string, any> | undefined): VideoMeetingVO {
|
// 从消息extra中提取meetingId
|
||||||
if (!contentExtra) {
|
function getMeetingId(contentExtra: Record<string, any> | undefined): string {
|
||||||
return {} as VideoMeetingVO
|
if (!contentExtra || !contentExtra.meetingId) {
|
||||||
|
console.warn('[ChatRoom] contentExtra中没有meetingId:', contentExtra)
|
||||||
|
return ''
|
||||||
}
|
}
|
||||||
return contentExtra as VideoMeetingVO
|
return contentExtra.meetingId as string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Markdown渲染函数
|
// Markdown渲染函数
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
// 显示模式选择器
|
// 显示模式选择器
|
||||||
showModeSelector() {
|
showModeSelector() {
|
||||||
uni.showActionSheet({
|
uni.showActionSheet({
|
||||||
itemList: ['员工模式 (17857100375)', '访客模式 (17857100376)'],
|
itemList: ['员工模式 (17857100375)', '访客模式 (17857100377)'],
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
let wechatId = ''
|
let wechatId = ''
|
||||||
let userMode = ''
|
let userMode = ''
|
||||||
@@ -56,8 +56,8 @@
|
|||||||
phone = '17857100375'
|
phone = '17857100375'
|
||||||
userMode = 'staff'
|
userMode = 'staff'
|
||||||
} else {
|
} else {
|
||||||
wechatId = '17857100376'
|
wechatId = '17857100377'
|
||||||
phone = '17857100376'
|
phone = '17857100377'
|
||||||
userMode = 'guest'
|
userMode = 'guest'
|
||||||
}
|
}
|
||||||
// 存储选择
|
// 存储选择
|
||||||
@@ -73,7 +73,7 @@
|
|||||||
fail: () => {
|
fail: () => {
|
||||||
// 用户取消,默认使用访客模式
|
// 用户取消,默认使用访客模式
|
||||||
uni.setStorageSync('userMode', 'guest')
|
uni.setStorageSync('userMode', 'guest')
|
||||||
uni.setStorageSync('wechatId', '17857100376')
|
uni.setStorageSync('wechatId', '17857100377')
|
||||||
console.log('默认使用访客模式')
|
console.log('默认使用访客模式')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/meeting/MeetingCreate",
|
"path": "pages/meeting/meetingCreate/MeetingCreate",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationStyle": "custom",
|
"navigationStyle": "custom",
|
||||||
"navigationBarTitleText": "创建会议"
|
"navigationBarTitleText": "创建会议"
|
||||||
|
|||||||
@@ -66,7 +66,7 @@
|
|||||||
</view>
|
</view>
|
||||||
<!-- 会议消息卡片 -->
|
<!-- 会议消息卡片 -->
|
||||||
<view class="message-content meeting-card-wrapper" v-if="msg.messageType === 'meet' && msg.contentExtra">
|
<view class="message-content meeting-card-wrapper" v-if="msg.messageType === 'meet' && msg.contentExtra">
|
||||||
<MeetingCard :meeting="getMeetingData(msg.contentExtra)" @join="handleJoinMeeting" />
|
<MeetingCard :meetingId="msg.content" @join="handleJoinMeeting" />
|
||||||
<text class="message-time">{{ formatTime(msg.sendTime) }}</text>
|
<text class="message-time">{{ formatTime(msg.sendTime) }}</text>
|
||||||
</view>
|
</view>
|
||||||
<!-- 普通消息 -->
|
<!-- 普通消息 -->
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
<view class="message-row self-row" v-else>
|
<view class="message-row self-row" v-else>
|
||||||
<!-- 会议消息卡片 -->
|
<!-- 会议消息卡片 -->
|
||||||
<view class="message-content meeting-card-wrapper" v-if="msg.messageType === 'meet' && msg.contentExtra">
|
<view class="message-content meeting-card-wrapper" v-if="msg.messageType === 'meet' && msg.contentExtra">
|
||||||
<MeetingCard :meeting="getMeetingData(msg.contentExtra)" @join="handleJoinMeeting" />
|
<MeetingCard :meetingId="msg.content" @join="handleJoinMeeting" />
|
||||||
<text class="message-time">{{ formatTime(msg.sendTime) }}</text>
|
<text class="message-time">{{ formatTime(msg.sendTime) }}</text>
|
||||||
</view>
|
</view>
|
||||||
<!-- 普通消息 -->
|
<!-- 普通消息 -->
|
||||||
@@ -410,11 +410,13 @@ function formatTime(time?: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取会议数据(将contentExtra转换为VideoMeetingVO)
|
// 获取会议数据(将contentExtra转换为VideoMeetingVO)
|
||||||
function getMeetingData(contentExtra: Record<string, any> | undefined): VideoMeetingVO {
|
// 从消息extra中提取meetingId
|
||||||
if (!contentExtra) {
|
function getMeetingId(contentExtra: Record<string, any> | undefined): string {
|
||||||
return {} as VideoMeetingVO
|
if (!contentExtra || !contentExtra.meetingId) {
|
||||||
|
console.warn('[chatRoom] contentExtra中没有meetingId:', contentExtra)
|
||||||
|
return ''
|
||||||
}
|
}
|
||||||
return contentExtra as VideoMeetingVO
|
return contentExtra.meetingId as string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Markdown渲染函数(返回富文本HTML)
|
// Markdown渲染函数(返回富文本HTML)
|
||||||
@@ -553,119 +555,20 @@ function handleWorkcaseAction() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 发起会议 - 跳转到会议创建页面
|
// 发起会议 - 跳转到会议创建页面
|
||||||
async function startMeeting() {
|
function startMeeting() {
|
||||||
// 先检查是否有活跃会议
|
// 跳转到会议创建页面
|
||||||
try {
|
const url = `/pages/meeting/meetingCreate/MeetingCreate?roomId=${roomId.value}${workcaseId.value ? '&workcaseId=' + workcaseId.value : ''}`
|
||||||
const res = await workcaseChatAPI.getActiveMeeting(roomId.value)
|
console.log('[chatRoom] 跳转会议创建页面:', url)
|
||||||
if (res.success && res.data) {
|
uni.navigateTo({
|
||||||
// 已有活跃会议,直接加入
|
url: url,
|
||||||
const meetingPageUrl = res.data.iframeUrl
|
fail: (err) => {
|
||||||
const meetingId = res.data.meetingId
|
console.error('[chatRoom] 跳转会议创建页面失败:', err)
|
||||||
const meetingName = res.data.meetingName || '视频会议'
|
|
||||||
|
|
||||||
// 构建完整的会议URL(包含域名和workcase路径)
|
|
||||||
const protocol = window.location.protocol
|
|
||||||
const host = window.location.host
|
|
||||||
const fullPath = meetingPageUrl.startsWith('/workcase')
|
|
||||||
? meetingPageUrl
|
|
||||||
: '/workcase' + meetingPageUrl
|
|
||||||
// 附加roomId参数,用于离开会议后返回聊天室
|
|
||||||
const fullMeetingUrl = `${protocol}//${host}${fullPath}&roomId=${roomId.value}`
|
|
||||||
|
|
||||||
// 小程序环境:显示提示,引导用户复制链接在浏览器打开
|
|
||||||
uni.showModal({
|
|
||||||
title: '视频会议',
|
|
||||||
content: '微信小程序暂不支持视频会议,请复制链接在浏览器中打开',
|
|
||||||
confirmText: '复制链接',
|
|
||||||
cancelText: '取消',
|
|
||||||
success: (res) => {
|
|
||||||
if (res.confirm) {
|
|
||||||
uni.setClipboardData({
|
|
||||||
data: fullMeetingUrl,
|
|
||||||
success: () => {
|
|
||||||
uni.showToast({
|
|
||||||
title: '链接已复制,请在浏览器中打开',
|
|
||||||
icon: 'none',
|
|
||||||
duration: 3000
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log('[chatRoom] 无活跃会议')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 没有活跃会议,创建新会议
|
|
||||||
try {
|
|
||||||
const createRes = await workcaseChatAPI.createMeeting({
|
|
||||||
roomId: roomId.value,
|
|
||||||
meetingName: roomName.value || '视频会议'
|
|
||||||
})
|
|
||||||
|
|
||||||
if (createRes.success && createRes.data) {
|
|
||||||
const meetingId = createRes.data.meetingId
|
|
||||||
|
|
||||||
// 开始会议
|
|
||||||
await workcaseChatAPI.startMeeting(meetingId)
|
|
||||||
|
|
||||||
// 加入会议获取会议页面URL
|
|
||||||
const joinRes = await workcaseChatAPI.joinMeeting(meetingId)
|
|
||||||
if (joinRes.success && joinRes.data && joinRes.data.iframeUrl) {
|
|
||||||
const meetingPageUrl = joinRes.data.iframeUrl
|
|
||||||
|
|
||||||
// 构建完整的会议URL(包含域名和workcase路径)
|
|
||||||
const protocol = window.location.protocol
|
|
||||||
const host = window.location.host
|
|
||||||
const fullPath = meetingPageUrl.startsWith('/workcase')
|
|
||||||
? meetingPageUrl
|
|
||||||
: '/workcase' + meetingPageUrl
|
|
||||||
// 附加roomId参数,用于离开会议后返回聊天室
|
|
||||||
const fullMeetingUrl = `${protocol}//${host}${fullPath}&roomId=${roomId.value}`
|
|
||||||
|
|
||||||
// 小程序环境:显示提示,引导用户复制链接在浏览器打开
|
|
||||||
uni.showModal({
|
|
||||||
title: '会议已创建',
|
|
||||||
content: '微信小程序暂不支持视频会议,请复制链接在浏览器中打开',
|
|
||||||
confirmText: '复制链接',
|
|
||||||
cancelText: '取消',
|
|
||||||
success: (res) => {
|
|
||||||
if (res.confirm) {
|
|
||||||
uni.setClipboardData({
|
|
||||||
data: fullMeetingUrl,
|
|
||||||
success: () => {
|
|
||||||
uni.showToast({
|
|
||||||
title: '链接已复制,请在浏览器中打开',
|
|
||||||
icon: 'none',
|
|
||||||
duration: 3000
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
uni.showToast({
|
|
||||||
title: '获取会议链接失败',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: createRes.message || '创建会议失败',
|
title: '跳转失败',
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch (error) {
|
})
|
||||||
console.error('[chatRoom] 创建会议失败:', error)
|
|
||||||
uni.showToast({
|
|
||||||
title: '创建会议失败',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加入会议(从MeetingCard点击加入)
|
// 加入会议(从MeetingCard点击加入)
|
||||||
@@ -794,9 +697,10 @@ function disconnectWebSocket() {
|
|||||||
function handleNewMessage(message: ChatRoomMessageVO) {
|
function handleNewMessage(message: ChatRoomMessageVO) {
|
||||||
console.log('[chatRoom] 收到新消息:', message)
|
console.log('[chatRoom] 收到新消息:', message)
|
||||||
|
|
||||||
// 避免重复添加自己发送的消息(自己发送的消息已经通过sendMessage添加到列表)
|
// 避免重复添加自己发送的普通消息(自己发送的消息已经通过sendMessage添加到列表)
|
||||||
if (message.senderId === currentUserId.value) {
|
// 但会议消息(meet类型)始终添加,因为它是系统生成的通知
|
||||||
console.log('[chatRoom] 跳过自己发送的消息')
|
if (message.messageType !== 'meet' && message.senderId === currentUserId.value) {
|
||||||
|
console.log('[chatRoom] 跳过自己发送的普通消息')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- 消息会议卡片 -->
|
<!-- 消息会议卡片 -->
|
||||||
<view :class="['meeting-card', meeting.status ? `meeting-card--${meeting.status}` : '']">
|
<view v-if="loading" class="meeting-card meeting-card--loading">
|
||||||
|
<text class="meeting-card-loading">加载中...</text>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="meeting" :class="['meeting-card', meeting.status ? `meeting-card--${meeting.status}` : '']">
|
||||||
<view class="meeting-card-header">
|
<view class="meeting-card-header">
|
||||||
<view class="meeting-card-title">{{ meeting.meetingName || '未命名会议' }}</view>
|
<view class="meeting-card-title">{{ meeting.meetingName || '未命名会议' }}</view>
|
||||||
<view class="meeting-card-status">
|
<view class="meeting-card-status">
|
||||||
@@ -29,36 +32,76 @@
|
|||||||
</button>
|
</button>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
<view v-else class="meeting-card meeting-card--error">
|
||||||
|
<text class="meeting-card-error">会议信息加载失败</text>
|
||||||
|
</view>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { VideoMeetingVO } from '../../../types/workcase/chatRoom'
|
import type { VideoMeetingVO } from '../../../types/workcase/chatRoom'
|
||||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { workcaseChatAPI } from '../../../api/workcase'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
meeting: VideoMeetingVO
|
meetingId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
console.log("meeting", JSON.stringify(props.meeting))
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
join: [meetingId: string]
|
join: [meetingId: string]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
// 会议详情(从API获取的实时数据)
|
||||||
|
const meeting = ref<VideoMeetingVO | null>(null)
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
// 当前时间,每秒更新
|
// 当前时间,每秒更新
|
||||||
const currentTime = ref(Date.now())
|
const currentTime = ref(Date.now())
|
||||||
let timer: ReturnType<typeof setInterval> | null = null
|
let timeTimer: ReturnType<typeof setInterval> | null = null
|
||||||
|
let refreshTimer: ReturnType<typeof setInterval> | null = null
|
||||||
|
|
||||||
|
// 获取会议详情
|
||||||
|
async function fetchMeetingInfo() {
|
||||||
|
if (!props.meetingId) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const result = await workcaseChatAPI.getMeetingInfo(props.meetingId)
|
||||||
|
if (result.success && result.data) {
|
||||||
|
meeting.value = result.data
|
||||||
|
console.log('[MeetingCard] 获取会议信息成功:', result.data)
|
||||||
|
} else {
|
||||||
|
console.error('[MeetingCard] 获取会议信息失败:', result.message)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[MeetingCard] 获取会议信息异常:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
// 初始加载会议信息
|
||||||
|
fetchMeetingInfo()
|
||||||
|
|
||||||
// 每秒更新当前时间
|
// 每秒更新当前时间
|
||||||
timer = setInterval(() => {
|
timeTimer = setInterval(() => {
|
||||||
currentTime.value = Date.now()
|
currentTime.value = Date.now()
|
||||||
}, 1000)
|
}, 1000)
|
||||||
|
|
||||||
|
// 每30秒刷新会议状态
|
||||||
|
refreshTimer = setInterval(() => {
|
||||||
|
fetchMeetingInfo()
|
||||||
|
}, 30000)
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (timer !== null) {
|
if (timeTimer !== null) {
|
||||||
clearInterval(timer)
|
clearInterval(timeTimer)
|
||||||
timer = null
|
timeTimer = null
|
||||||
|
}
|
||||||
|
if (refreshTimer !== null) {
|
||||||
|
clearInterval(refreshTimer)
|
||||||
|
refreshTimer = null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -82,23 +125,17 @@ function formatDateTime(dateStr?: string): string {
|
|||||||
* 计算倒计时文本
|
* 计算倒计时文本
|
||||||
*/
|
*/
|
||||||
const countdownText = computed((): string => {
|
const countdownText = computed((): string => {
|
||||||
const meeting = props.meeting
|
if (!meeting.value || !meeting.value.startTime || !meeting.value.endTime) {
|
||||||
if (!meeting) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查必要字段
|
|
||||||
if (!meeting.startTime || !meeting.endTime) {
|
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
// advance 默认为 0
|
// advance 默认为 0
|
||||||
const advanceMinutes = meeting.advance || 0
|
const advanceMinutes = meeting.value.advance || 0
|
||||||
|
|
||||||
const now = currentTime.value
|
const now = currentTime.value
|
||||||
// iOS和小程序兼容性处理
|
// iOS和小程序兼容性处理
|
||||||
const startTime = new Date(meeting.startTime.replace(' ', 'T')).getTime()
|
const startTime = new Date(meeting.value.startTime.replace(' ', 'T')).getTime()
|
||||||
const endTime = new Date(meeting.endTime.replace(' ', 'T')).getTime()
|
const endTime = new Date(meeting.value.endTime.replace(' ', 'T')).getTime()
|
||||||
|
|
||||||
// 检查时间解析是否有效
|
// 检查时间解析是否有效
|
||||||
if (isNaN(startTime) || isNaN(endTime)) {
|
if (isNaN(startTime) || isNaN(endTime)) {
|
||||||
@@ -107,7 +144,7 @@ const countdownText = computed((): string => {
|
|||||||
|
|
||||||
const advanceTime = startTime - advanceMinutes * 60 * 1000
|
const advanceTime = startTime - advanceMinutes * 60 * 1000
|
||||||
|
|
||||||
if (meeting.status === 'ended') {
|
if (meeting.value.status === 'ended') {
|
||||||
return '会议已结束'
|
return '会议已结束'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +168,7 @@ const countdownText = computed((): string => {
|
|||||||
return '可以入会'
|
return '可以入会'
|
||||||
} else if (now < endTime) {
|
} else if (now < endTime) {
|
||||||
// 会议进行中
|
// 会议进行中
|
||||||
if (meeting.status === 'ongoing') {
|
if (meeting.value.status === 'ongoing') {
|
||||||
return '会议进行中'
|
return '会议进行中'
|
||||||
} else {
|
} else {
|
||||||
return '可以入会'
|
return '可以入会'
|
||||||
@@ -146,24 +183,19 @@ const countdownText = computed((): string => {
|
|||||||
* 是否可以加入会议
|
* 是否可以加入会议
|
||||||
*/
|
*/
|
||||||
const canJoinMeeting = computed((): boolean => {
|
const canJoinMeeting = computed((): boolean => {
|
||||||
const meeting = props.meeting
|
if (!meeting.value || !meeting.value.startTime || !meeting.value.endTime) {
|
||||||
if (!meeting) {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!meeting.startTime || !meeting.endTime) {
|
if (meeting.value.status === 'ended') {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (meeting.status === 'ended') {
|
const advanceMinutes = meeting.value.advance || 0
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const advanceMinutes = meeting.advance || 0
|
|
||||||
const now = currentTime.value
|
const now = currentTime.value
|
||||||
// iOS和小程序兼容性处理
|
// iOS和小程序兼容性处理
|
||||||
const startTime = new Date(meeting.startTime.replace(' ', 'T')).getTime()
|
const startTime = new Date(meeting.value.startTime.replace(' ', 'T')).getTime()
|
||||||
const endTime = new Date(meeting.endTime.replace(' ', 'T')).getTime()
|
const endTime = new Date(meeting.value.endTime.replace(' ', 'T')).getTime()
|
||||||
|
|
||||||
// 检查时间解析是否有效
|
// 检查时间解析是否有效
|
||||||
if (isNaN(startTime) || isNaN(endTime)) {
|
if (isNaN(startTime) || isNaN(endTime)) {
|
||||||
@@ -180,8 +212,10 @@ const canJoinMeeting = computed((): boolean => {
|
|||||||
* 按钮文本
|
* 按钮文本
|
||||||
*/
|
*/
|
||||||
const buttonText = computed((): string => {
|
const buttonText = computed((): string => {
|
||||||
const meeting = props.meeting
|
if (!meeting.value) {
|
||||||
if (meeting.status === 'ended') {
|
return '加载中'
|
||||||
|
}
|
||||||
|
if (meeting.value.status === 'ended') {
|
||||||
return '会议已结束'
|
return '会议已结束'
|
||||||
}
|
}
|
||||||
if (!canJoinMeeting.value) {
|
if (!canJoinMeeting.value) {
|
||||||
@@ -195,12 +229,12 @@ const buttonText = computed((): string => {
|
|||||||
*/
|
*/
|
||||||
function handleJoinMeeting() {
|
function handleJoinMeeting() {
|
||||||
console.log('[MeetingCard] handleJoinMeeting 被点击', {
|
console.log('[MeetingCard] handleJoinMeeting 被点击', {
|
||||||
meetingId: props.meeting.meetingId,
|
meetingId: props.meetingId,
|
||||||
canJoin: canJoinMeeting.value,
|
canJoin: canJoinMeeting.value,
|
||||||
meeting: props.meeting
|
meeting: meeting.value
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!props.meeting.meetingId) {
|
if (!props.meetingId) {
|
||||||
console.error('[MeetingCard] 会议ID不存在')
|
console.error('[MeetingCard] 会议ID不存在')
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '会议ID不存在',
|
title: '会议ID不存在',
|
||||||
@@ -211,14 +245,14 @@ function handleJoinMeeting() {
|
|||||||
|
|
||||||
if (!canJoinMeeting.value) {
|
if (!canJoinMeeting.value) {
|
||||||
console.warn('[MeetingCard] 不允许加入会议', {
|
console.warn('[MeetingCard] 不允许加入会议', {
|
||||||
status: props.meeting.status,
|
status: meeting.value?.status,
|
||||||
canJoin: canJoinMeeting.value
|
canJoin: canJoinMeeting.value
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[MeetingCard] 触发join事件, meetingId:', props.meeting.meetingId)
|
console.log('[MeetingCard] 触发join事件, meetingId:', props.meetingId)
|
||||||
emit('join', props.meeting.meetingId)
|
emit('join', props.meetingId)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
.meeting-create-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
padding-bottom: 120rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 32rpx;
|
||||||
|
border-bottom: 1px solid #ebeef5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container {
|
||||||
|
background-color: #fff;
|
||||||
|
margin-top: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item {
|
||||||
|
padding: 24rpx 32rpx;
|
||||||
|
border-bottom: 1px solid #ebeef5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item.required .label-text::after {
|
||||||
|
content: ' *';
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #606266;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.required-star {
|
||||||
|
color: #f56c6c;
|
||||||
|
margin-left: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 16rpx 24rpx;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-display {
|
||||||
|
padding: 16rpx 24rpx;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-display .placeholder {
|
||||||
|
color: #c0c4cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-tip {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-tip text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-footer {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 24rpx 32rpx;
|
||||||
|
background-color: #fff;
|
||||||
|
border-top: 1px solid #ebeef5;
|
||||||
|
box-shadow: 0 -2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 24rpx 0;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
font-size: 32rpx;
|
||||||
|
text-align: center;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
color: #606266;
|
||||||
|
margin-right: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-submit {
|
||||||
|
background-color: #409eff;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-submit[loading] {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
@@ -114,8 +114,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, computed } from 'vue'
|
import { ref, reactive, computed } from 'vue'
|
||||||
import { onLoad } from '@dcloudio/uni-app'
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
import { workcaseChatAPI } from '../../api/workcase/workcaseChat'
|
import { workcaseChatAPI } from '../../../api/workcase/workcaseChat'
|
||||||
import type { CreateMeetingParam } from '../../types/workcase/chatRoom'
|
import type { CreateMeetingParam } from '../../../types/workcase/chatRoom'
|
||||||
|
|
||||||
// 路由参数
|
// 路由参数
|
||||||
const roomId = ref('')
|
const roomId = ref('')
|
||||||
@@ -328,120 +328,5 @@ function handleCancel() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.meeting-create-page {
|
@import url('./MeetingCreate.scss')
|
||||||
min-height: 100vh;
|
|
||||||
background-color: #f5f7fa;
|
|
||||||
padding-bottom: 120rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header {
|
|
||||||
background-color: #fff;
|
|
||||||
padding: 32rpx;
|
|
||||||
border-bottom: 1px solid #ebeef5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-title {
|
|
||||||
font-size: 36rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #303133;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-container {
|
|
||||||
background-color: #fff;
|
|
||||||
margin-top: 16rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-item {
|
|
||||||
padding: 24rpx 32rpx;
|
|
||||||
border-bottom: 1px solid #ebeef5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-item.required .label-text::after {
|
|
||||||
content: ' *';
|
|
||||||
color: #f56c6c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-label {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 16rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label-text {
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: #606266;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.required-star {
|
|
||||||
color: #f56c6c;
|
|
||||||
margin-left: 8rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 16rpx 24rpx;
|
|
||||||
border: 1px solid #dcdfe6;
|
|
||||||
border-radius: 8rpx;
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: #303133;
|
|
||||||
}
|
|
||||||
|
|
||||||
.picker-display {
|
|
||||||
padding: 16rpx 24rpx;
|
|
||||||
border: 1px solid #dcdfe6;
|
|
||||||
border-radius: 8rpx;
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: #303133;
|
|
||||||
}
|
|
||||||
|
|
||||||
.picker-display .placeholder {
|
|
||||||
color: #c0c4cc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-tip {
|
|
||||||
margin-top: 8rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-tip text {
|
|
||||||
font-size: 24rpx;
|
|
||||||
color: #909399;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-footer {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 24rpx 32rpx;
|
|
||||||
background-color: #fff;
|
|
||||||
border-top: 1px solid #ebeef5;
|
|
||||||
box-shadow: 0 -2rpx 8rpx rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
flex: 1;
|
|
||||||
padding: 24rpx 0;
|
|
||||||
border-radius: 8rpx;
|
|
||||||
font-size: 32rpx;
|
|
||||||
text-align: center;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-cancel {
|
|
||||||
background-color: #f5f7fa;
|
|
||||||
color: #606266;
|
|
||||||
margin-right: 16rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-submit {
|
|
||||||
background-color: #409eff;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-submit[loading] {
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
Reference in New Issue
Block a user