稳定测试版

main
wang 1 month ago
parent 36b003d5b9
commit f546ce7031

@ -1,39 +1,74 @@
server {
listen 80;
listen 80 default_server;
listen [::]:80 default_server;
# 开启 HTTP/2
http2 on;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# 安全设置
# ================= 安全相关的Header =================
server_tokens off;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# 允许跨域访问
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE' always;
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always;
root /usr/share/nginx/html;
index index.html;
# ================= 性能优化Gzip压缩 =================
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_min_length 256;
gzip_types
application/atom+xml
application/javascript
application/json
application/ld+json
application/manifest+json
application/rss+xml
application/vnd.geo+json
application/vnd.ms-fontobject
application/x-font-ttf
application/x-web-app-manifest+json
application/xhtml+xml
application/xml
font/opentype
image/bmp
image/svg+xml
image/x-icon
text/cache-manifest
text/css
text/plain
text/vcard
text/vnd.rim.location.xloc
text/vtt
text/x-component
text/x-cross-domain-policy;
# 设置文件缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires max;
add_header Cache-Control "public, max-age=31536000";
access_log off;
# ================= 性能优化:浏览器缓存 =================
# 对多媒体文件和字体文件进行长期缓存
location ~* \.(?:jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc|woff|woff2|ttf|eot)$ {
expires 1M;
access_log off;
add_header Cache-Control "public";
}
# 特别处理images目录
location /images/ {
alias /usr/share/nginx/html/images/;
autoindex off;
expires max;
add_header Cache-Control "public, max-age=31536000";
try_files $uri $uri/ =404;
# 对JS和CSS文件进行长期缓存
location ~* \.(?:css|js)$ {
expires 1y;
access_log off;
add_header Cache-Control "public";
}
# 静态资源请求直接访问
# 对于根路径和HTML文件不进行强缓存确保用户能获取最新版本
location / {
try_files $uri $uri/ /index.html;
add_header Cache-Control "no-cache, no-store, must-revalidate";
@ -48,7 +83,7 @@ server {
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 90s;
proxy_connect_timeout 90s;
proxy_buffering off;
proxy_buffering off; # 对API请求关闭缓冲确保实时性
}
# Socket.IO请求代理到后端服务
@ -61,13 +96,13 @@ server {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_cache_bypass $http_upgrade;
proxy_read_timeout 86400s; # 设置更长的读取超时适合WebSocket连接
proxy_send_timeout 86400s; # 增加发送超时
proxy_connect_timeout 7d; # 增加连接超时
proxy_read_timeout 7d;
proxy_send_timeout 7d;
proxy_connect_timeout 7d;
proxy_buffering off;
}
# 支持UDP媒体流通过TURN服务器转发
# 支持WebRTC信令转发
location /rtc/ {
proxy_pass http://backend:5000;
proxy_http_version 1.1;
@ -75,14 +110,10 @@ server {
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 86400s;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 7d;
}
# 压缩设置
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
gzip_min_length 1000;
# 错误页面
error_page 404 /index.html;
error_page 500 502 503 504 /50x.html;

File diff suppressed because one or more lines are too long

@ -66,45 +66,53 @@
v-for="msg in messages"
:key="msg.id"
class="message-item"
:class="{ 'current-user-message': isCurrentUser(msg.user_id) }"
:class="{ 'system-notification': msg.isSystem }"
>
<div class="message-header" :class="{ 'current-user-header': isCurrentUser(msg.user_id) }">
<span class="username" :class="{ 'current-user': isCurrentUser(msg.user_id) }">
{{ msg.username }}
</span>
<span class="timestamp">{{ formatTime(msg.created_at) }}</span>
</div>
<!-- 文本消息 -->
<div v-if="msg.message_type === 'text' || !msg.message_type"
class="message-content"
:class="{ 'current-user-content': isCurrentUser(msg.user_id) }">
<!-- 系统消息的特殊渲染 -->
<div v-if="msg.isSystem" class="notification-content">
{{ msg.message }}
</div>
<!-- 语音消息 -->
<div v-else-if="msg.message_type === 'audio'"
class="message-content audio-message"
:class="{
'current-user-content': isCurrentUser(msg.user_id),
'local-message': msg.isLocal
}">
<div class="audio-player">
<button @click="playAudio(msg)"
class="play-button"
:class="{
'playing': isMessagePlaying(msg.id),
'local': msg.isLocal
}">
<i class="audio-icon">{{ isMessagePlaying(msg.id) ? '⏸️' : '🔊' }}</i>
</button>
<div class="audio-waveform"
:class="{
'playing': isMessagePlaying(msg.id),
'local': msg.isLocal
}">
<div class="duration">
{{ formatAudioDuration(msg.audio_duration) }}
<!-- 用户消息的正常渲染 -->
<div v-else class="user-message-content" :class="{ 'current-user-message': isCurrentUser(msg.user_id) }">
<div class="message-header" :class="{ 'current-user-header': isCurrentUser(msg.user_id) }">
<span class="username" :class="{ 'current-user': isCurrentUser(msg.user_id) }">
{{ msg.username }}
</span>
<span class="timestamp">{{ formatTime(msg.created_at) }}</span>
</div>
<!-- 文本消息 -->
<div v-if="msg.message_type === 'text' || !msg.message_type"
class="message-content"
:class="{ 'current-user-content': isCurrentUser(msg.user_id) }">
{{ msg.message }}
</div>
<!-- 语音消息 -->
<div v-else-if="msg.message_type === 'audio'"
class="message-content audio-message"
:class="{
'current-user-content': isCurrentUser(msg.user_id),
'local-message': msg.isLocal
}">
<div class="audio-player">
<button @click="playAudio(msg)"
class="play-button"
:class="{
'playing': isMessagePlaying(msg.id),
'local': msg.isLocal
}">
<i class="audio-icon">{{ isMessagePlaying(msg.id) ? '⏸️' : '🔊' }}</i>
</button>
<div class="audio-waveform"
:class="{
'playing': isMessagePlaying(msg.id),
'local': msg.isLocal
}">
<div class="duration">
{{ formatAudioDuration(msg.audio_duration) }}
</div>
</div>
</div>
</div>
@ -114,10 +122,6 @@
<div v-if="messages.length === 0" class="no-messages">
<p>暂无消息开始聊天吧</p>
</div>
<div v-for="notification in notifications" :key="notification.id" class="notification">
{{ notification.message }}
</div>
</div>
<!-- 语音聊天参与者列表 -->
@ -215,6 +219,7 @@
<script>
import axios from 'axios'
import { io } from 'socket.io-client'
// 使 'computed'
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
export default {
@ -225,7 +230,7 @@ export default {
const rooms = ref([])
const currentRoom = ref(null)
const messages = ref([])
const notifications = ref([])
// 使 'notifications' 'notificationTimeout'
const newMessage = ref('')
const newRoomName = ref('')
const showCreateRoomModal = ref(false)
@ -262,7 +267,6 @@ export default {
let socket = null
let currentUser = null
let notificationCounter = 0
let audioAnalyser = null
let audioAnalyserInterval = null
@ -548,25 +552,23 @@ export default {
//
const joinRoom = async (room) => {
if (currentRoom.value && currentRoom.value.id === room.id) return;
if (currentRoom.value) {
leaveRoom()
}
currentRoom.value = room
messages.value = [] //
// notifications.value = [] //
try {
currentRoom.value = room
messages.value = []
// Socket.IO
socket.emit('join_room', { room_id: room.id })
//
const response = await axios.get(`/api/chat/rooms/${room.id}/messages`)
messages.value = response.data.messages
// WebRTC
if (rtcActive.value) {
initWebRTC()
}
scrollToBottom()
} catch (error) {
console.error('加入聊天室失败:', error)
messages.value = response.data.messages.map(m => ({ ...m, isSystem: false }))
socket.emit('join_room', { room_id: room.id })
} catch (err) {
error.value = '无法加载聊天记录'
currentRoom.value = null
}
}
@ -750,46 +752,23 @@ export default {
}
//
const stopRecording = () => {
if (!mediaRecorder.value || mediaRecorder.value.state === 'inactive') {
return
}
//
mediaRecorder.value.stop()
//
if (recordingTimer.value) {
clearInterval(recordingTimer.value)
recordingTimer.value = null
}
}
//
const cancelRecording = () => {
if (isRecording.value) {
const stopRecording = (forceStop = false) => {
if (mediaRecorder.value && isRecording.value) {
mediaRecorder.value.stop()
isRecording.value = false
if (mediaRecorder.value && mediaRecorder.value.state === 'recording') {
mediaRecorder.value.stop()
}
//
if (recordingTimer.value) {
clearInterval(recordingTimer.value)
recordingTimer.value = null
}
recordingDuration.value = 0
audioChunks.value = []
//
if (!forceStop) {
// onstop
} else {
audioChunks.value = [] //
}
}
}
//
const stopMediaTracks = (stream) => {
stream.getTracks().forEach(track => track.stop())
}
//
const playAudio = (message) => {
if (!message || message.message_type !== 'audio') return
@ -938,12 +917,11 @@ export default {
//
const addNotification = (message) => {
notifications.value.push({
id: `notification-${notificationCounter++}`,
messages.value.push({
id: `system-${Date.now()}`,
message,
timestamp: new Date()
isSystem: true,
})
scrollToBottom()
}
@ -1401,13 +1379,19 @@ export default {
})
}
// stopMediaTracks
const stopMediaTracks = (stream) => {
if (stream) {
stream.getTracks().forEach(track => track.stop());
}
};
return {
loading,
error,
rooms,
currentRoom,
messages,
notifications,
newMessage,
newRoomName,
showCreateRoomModal,
@ -1415,32 +1399,29 @@ export default {
audioPlayer,
isRecording,
recordingDuration,
currentPlayingId,
isPlaying,
rtcActive,
localStream,
peerConnections,
mediaDevices,
selectedAudioInput,
selectedAudioOutput,
audioLevel,
isMuted,
isLoadingRTC,
rtcError,
audioLevel,
rtcParticipants,
rtcError,
fetchRooms,
createRoom,
joinRoom,
leaveRoom,
sendMessage,
createRoom,
isCurrentUser,
formatTime,
toggleRecording,
stopRecording,
cancelRecording,
playAudio,
isMessagePlaying,
formatAudioDuration,
isCurrentUser,
formatTime,
toggleVoiceChat,
toggleMute
toggleMute,
// 'latestNotification'
}
}
}
@ -1622,12 +1603,32 @@ export default {
}
.message-item {
max-width: 80%;
align-self: flex-start;
margin: 10px 0;
display: flex; /* Use flexbox for alignment */
}
.current-user-message {
align-self: flex-end;
.message-item.system-notification {
justify-content: center;
}
.notification-content {
background-color: #e9ecef;
color: #888;
font-size: 0.85em;
padding: 5px 12px;
border-radius: 12px;
text-align: center;
}
.user-message-content {
max-width: 70%;
display: flex;
flex-direction: column;
}
.user-message-content.current-user-message {
margin-left: auto; /* Push to the right */
align-items: flex-end;
}
.message-header {
@ -2275,4 +2276,16 @@ export default {
box-shadow: 0 0 0 0 rgba(139, 69, 19, 0);
}
}
.top-notification {
text-align: center;
padding: 8px;
background-color: #f8f9fa;
color: #6c757d;
font-size: 0.9em;
border-bottom: 1px solid #dee2e6;
position: sticky;
top: 0;
z-index: 10;
}
</style>
Loading…
Cancel
Save