jisti-meet服务开启
This commit is contained in:
@@ -1,471 +0,0 @@
|
||||
// 品牌色
|
||||
$brand-color: #0055AA;
|
||||
$brand-color-light: #EBF5FF;
|
||||
$brand-color-hover: #004488;
|
||||
|
||||
.chat-room-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
// ==================== 加载更多 ====================
|
||||
.loading-more {
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
font-size: 13px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
// ==================== 聊天室头部 ====================
|
||||
.chat-header {
|
||||
height: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 24px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
flex-shrink: 0;
|
||||
|
||||
.header-default {
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 消息容器 ====================
|
||||
.messages-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
background: #f8fafc;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
// ==================== Jitsi Meet会议容器 ====================
|
||||
.meeting-container {
|
||||
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);
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 消息列表 ====================
|
||||
.messages-list {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 24px 16px;
|
||||
|
||||
.message-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
|
||||
&.is-me {
|
||||
flex-direction: row-reverse;
|
||||
|
||||
.message-bubble {
|
||||
background: $brand-color;
|
||||
color: #fff;
|
||||
border-radius: 16px 16px 4px 16px;
|
||||
|
||||
.message-time {
|
||||
text-align: right;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.other {
|
||||
.message-bubble {
|
||||
background: #fff;
|
||||
border: 1px solid #f1f5f9;
|
||||
border-radius: 16px 16px 16px 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
background: $brand-color-light;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.avatar-text {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: $brand-color;
|
||||
}
|
||||
}
|
||||
|
||||
.sender-name {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.message-content-wrapper {
|
||||
max-width: 70%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.message-bubble {
|
||||
padding: 12px 16px;
|
||||
|
||||
.message-text {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.message-files {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.file-info {
|
||||
.file-name {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.other .file-item {
|
||||
background: #f8fafc;
|
||||
border-color: #e2e8f0;
|
||||
|
||||
&:hover {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
background: $brand-color-light;
|
||||
color: $brand-color;
|
||||
}
|
||||
|
||||
.file-info {
|
||||
color: #374151;
|
||||
}
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
padding: 0 4px;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 输入区域 ====================
|
||||
.input-area {
|
||||
padding: 16px 24px 24px;
|
||||
background: #fff;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
flex-shrink: 0;
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 16px;
|
||||
background: transparent;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-size: 14px;
|
||||
|
||||
&:hover {
|
||||
border-color: $brand-color;
|
||||
color: $brand-color;
|
||||
background: $brand-color-light;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.input-card {
|
||||
background: #f8fafc;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
overflow: hidden;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:focus-within {
|
||||
border-color: $brand-color;
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.input-row {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.chat-textarea {
|
||||
width: 100%;
|
||||
border: none;
|
||||
outline: none;
|
||||
resize: none;
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
background: transparent;
|
||||
line-height: 1.5;
|
||||
min-height: 60px;
|
||||
max-height: 150px;
|
||||
font-family: inherit;
|
||||
|
||||
&::placeholder {
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-row {
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.tool-btn {
|
||||
padding: 8px;
|
||||
color: #94a3b8;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
color: $brand-color;
|
||||
background: $brand-color-light;
|
||||
}
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
padding: 8px 16px;
|
||||
background: #e2e8f0;
|
||||
color: #94a3b8;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: not-allowed;
|
||||
transition: all 0.2s;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
&.active {
|
||||
background: $brand-color;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 8px rgba($brand-color, 0.3);
|
||||
|
||||
&:hover {
|
||||
background: $brand-color-hover;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Markdown样式 ====================
|
||||
.message-bubble {
|
||||
// 粗体
|
||||
strong {
|
||||
font-weight: 600;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
// 斜体
|
||||
em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
// 行内代码
|
||||
.inline-code {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
color: #e53e3e;
|
||||
}
|
||||
|
||||
// 代码块
|
||||
.code-block {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
margin: 8px 0;
|
||||
|
||||
code {
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: #334155;
|
||||
}
|
||||
}
|
||||
|
||||
// 链接
|
||||
.md-link {
|
||||
color: $brand-color;
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover {
|
||||
color: $brand-color-hover;
|
||||
}
|
||||
}
|
||||
|
||||
// 标题
|
||||
.md-h1 {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
margin: 12px 0 8px;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.md-h2 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin: 10px 0 6px;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.md-h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin: 8px 0 4px;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
// 列表
|
||||
.md-ul, .md-ol {
|
||||
margin: 8px 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.md-li {
|
||||
margin: 4px 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
@@ -1,380 +0,0 @@
|
||||
<template>
|
||||
<div class="chat-room-main">
|
||||
<!-- 聊天室头部 -->
|
||||
<header class="chat-header">
|
||||
<slot name="header">
|
||||
<div class="header-default">
|
||||
<h3>{{ roomName }}</h3>
|
||||
</div>
|
||||
</slot>
|
||||
</header>
|
||||
|
||||
<!-- 消息容器 -->
|
||||
<div ref="messagesRef" class="messages-container" @scroll="handleScroll">
|
||||
<!-- 加载更多提示 -->
|
||||
<div v-if="loadingMore" class="loading-more">加载中...</div>
|
||||
<div v-else-if="!hasMore" class="loading-more">没有更多消息了</div>
|
||||
|
||||
<!-- Jitsi Meet会议iframe -->
|
||||
<div v-if="showMeeting && meetingUrl" class="meeting-container">
|
||||
<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>
|
||||
|
||||
<!-- 聊天消息列表 -->
|
||||
<div class="messages-list">
|
||||
<div
|
||||
v-for="message in messages"
|
||||
:key="message.messageId"
|
||||
class="message-row"
|
||||
:class="message.senderId === currentUserId ? 'is-me' : 'other'"
|
||||
>
|
||||
<div>
|
||||
<!-- 头像 -->
|
||||
<div class="message-avatar">
|
||||
<img v-if="message.senderAvatar" :src="FILE_DOWNLOAD_URL + message.senderAvatar" />
|
||||
<span v-else class="avatar-text">{{ message.senderName?.charAt(0) || '?' }}</span>
|
||||
</div>
|
||||
<div class="sender-name">{{ message.senderName || '未知用户' }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 消息内容 -->
|
||||
<div class="message-content-wrapper">
|
||||
<div class="message-bubble">
|
||||
<div
|
||||
class="message-text"
|
||||
v-html="renderMarkdown(message.content || '')"
|
||||
></div>
|
||||
|
||||
<!-- 文件列表 -->
|
||||
<div v-if="message.files && message.files.length > 0" class="message-files">
|
||||
<div
|
||||
v-for="file in message.files"
|
||||
:key="file"
|
||||
class="file-item"
|
||||
@click="$emit('download-file', file)"
|
||||
>
|
||||
<div class="file-icon">
|
||||
<FileText :size="16" />
|
||||
</div>
|
||||
<div class="file-info">
|
||||
<div class="file-name">附件</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="message-time">{{ formatTime(message.sendTime) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 输入区域 -->
|
||||
<footer class="input-area">
|
||||
<!-- 操作按钮区域 -->
|
||||
<div class="action-buttons">
|
||||
<!-- 发起会议按钮 -->
|
||||
<button
|
||||
class="action-btn"
|
||||
:disabled="meetingLoading || showMeeting"
|
||||
@click="handleStartMeeting"
|
||||
>
|
||||
<Video :size="18" />
|
||||
{{ showMeeting ? '会议进行中' : '发起会议' }}
|
||||
</button>
|
||||
|
||||
<!-- 额外的操作按钮插槽 -->
|
||||
<slot name="action-area"></slot>
|
||||
</div>
|
||||
|
||||
<!-- 输入框 -->
|
||||
<div class="input-wrapper">
|
||||
<div class="input-card">
|
||||
<div class="input-row">
|
||||
<textarea
|
||||
ref="textareaRef"
|
||||
v-model="inputText"
|
||||
@input="adjustHeight"
|
||||
@keydown="handleKeyDown"
|
||||
placeholder="输入消息..."
|
||||
class="chat-textarea"
|
||||
/>
|
||||
</div>
|
||||
<div class="toolbar-row">
|
||||
<div class="toolbar-left">
|
||||
<button class="tool-btn" @click="selectFiles" title="上传文件">
|
||||
<Paperclip :size="18" />
|
||||
</button>
|
||||
<input
|
||||
ref="fileInputRef"
|
||||
type="file"
|
||||
multiple
|
||||
style="display: none"
|
||||
@change="handleFileSelect"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class="send-btn"
|
||||
:class="{ active: inputText.trim() }"
|
||||
:disabled="!inputText.trim()"
|
||||
@click="sendMessage"
|
||||
>
|
||||
<Send :size="18" />
|
||||
发送
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
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
|
||||
workcaseId?: string
|
||||
fileDownloadUrl?: string
|
||||
hasMore?: boolean
|
||||
loadingMore?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
roomName: '聊天室',
|
||||
fileDownloadUrl: '',
|
||||
hasMore: true,
|
||||
loadingMore: false
|
||||
})
|
||||
|
||||
const FILE_DOWNLOAD_URL = props.fileDownloadUrl
|
||||
|
||||
const emit = defineEmits<{
|
||||
'send-message': [content: string, files: File[]]
|
||||
'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
|
||||
if (target.scrollTop < 50 && props.hasMore && !props.loadingMore) {
|
||||
emit('load-more')
|
||||
}
|
||||
}
|
||||
|
||||
defineSlots<{
|
||||
header?: () => any
|
||||
'action-area'?: () => any
|
||||
}>()
|
||||
|
||||
const inputText = ref('')
|
||||
const selectedFiles = ref<File[]>([])
|
||||
const messagesRef = ref<HTMLElement | null>(null)
|
||||
const textareaRef = ref<HTMLTextAreaElement | null>(null)
|
||||
const fileInputRef = ref<HTMLInputElement | null>(null)
|
||||
|
||||
// 发送消息
|
||||
const sendMessage = () => {
|
||||
if (!inputText.value.trim() && selectedFiles.value.length === 0) return
|
||||
|
||||
emit('send-message', inputText.value.trim(), selectedFiles.value)
|
||||
|
||||
inputText.value = ''
|
||||
selectedFiles.value = []
|
||||
|
||||
if (textareaRef.value) {
|
||||
textareaRef.value.style.height = 'auto'
|
||||
}
|
||||
|
||||
scrollToBottom()
|
||||
}
|
||||
|
||||
// 选择文件
|
||||
const selectFiles = () => {
|
||||
fileInputRef.value?.click()
|
||||
}
|
||||
|
||||
// 处理文件选择
|
||||
const handleFileSelect = (e: Event) => {
|
||||
const files = (e.target as HTMLInputElement).files
|
||||
if (!files || files.length === 0) return
|
||||
|
||||
selectedFiles.value = Array.from(files)
|
||||
}
|
||||
|
||||
// 滚动到底部
|
||||
const scrollToBottom = () => {
|
||||
nextTick(() => {
|
||||
if (messagesRef.value) {
|
||||
messagesRef.value.scrollTop = messagesRef.value.scrollHeight
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 自动调整输入框高度
|
||||
const adjustHeight = () => {
|
||||
const el = textareaRef.value
|
||||
if (el) {
|
||||
el.style.height = 'auto'
|
||||
el.style.height = Math.min(el.scrollHeight, 150) + 'px'
|
||||
}
|
||||
}
|
||||
|
||||
// 键盘事件
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
sendMessage()
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (time?: string) => {
|
||||
if (!time) return ''
|
||||
const date = new Date(time)
|
||||
const now = new Date()
|
||||
const diff = now.getTime() - date.getTime()
|
||||
|
||||
if (diff < 60000) return '刚刚'
|
||||
if (diff < 3600000) return Math.floor(diff / 60000) + '分钟前'
|
||||
if (diff < 86400000) return Math.floor(diff / 3600000) + '小时前'
|
||||
|
||||
return date.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' })
|
||||
}
|
||||
|
||||
// Markdown渲染函数
|
||||
const renderMarkdown = (text: string): string => {
|
||||
if (!text) return ''
|
||||
|
||||
// 转义HTML标签
|
||||
let html = text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
|
||||
// 处理代码块(```语法)
|
||||
html = html.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => {
|
||||
return `<pre class="code-block"><code class="language-${lang || 'text'}">${code.trim()}</code></pre>`
|
||||
})
|
||||
|
||||
// 处理行内代码(`语法)
|
||||
html = html.replace(/`([^`]+)`/g, '<code class="inline-code">$1</code>')
|
||||
|
||||
// 处理粗体(**语法)
|
||||
html = html.replace(/\*\*([^\*]+)\*\*/g, '<strong>$1</strong>')
|
||||
|
||||
// 处理斜体(*语法)
|
||||
html = html.replace(/\*([^\*]+)\*/g, '<em>$1</em>')
|
||||
|
||||
// 处理链接([text](url)语法)
|
||||
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" class="md-link">$1</a>')
|
||||
|
||||
// 处理标题(# ## ###等)
|
||||
html = html.replace(/^### (.+)$/gm, '<h3 class="md-h3">$1</h3>')
|
||||
html = html.replace(/^## (.+)$/gm, '<h2 class="md-h2">$1</h2>')
|
||||
html = html.replace(/^# (.+)$/gm, '<h1 class="md-h1">$1</h1>')
|
||||
|
||||
// 处理无序列表(- 或 * 开头)
|
||||
html = html.replace(/^[*-] (.+)$/gm, '<li class="md-li">$1</li>')
|
||||
html = html.replace(/(<li class="md-li">.*<\/li>)/s, '<ul class="md-ul">$1</ul>')
|
||||
|
||||
// 处理有序列表(数字. 开头)
|
||||
html = html.replace(/^\d+\. (.+)$/gm, '<li class="md-li">$1</li>')
|
||||
|
||||
// 处理换行
|
||||
html = html.replace(/\n/g, '<br>')
|
||||
|
||||
return html
|
||||
}
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
scrollToBottom,
|
||||
handleStartMeeting,
|
||||
handleEndMeeting
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import url("./ChatRoom.scss");
|
||||
</style>
|
||||
@@ -1 +0,0 @@
|
||||
export { default as ChatRoom } from './chatRoom/ChatRoom.vue';
|
||||
@@ -1 +0,0 @@
|
||||
export * from './chatRoom'
|
||||
Reference in New Issue
Block a user