更新了一些功能(媒体预览有bug)
This commit is contained in:
@@ -1,33 +1,392 @@
|
||||
<template>
|
||||
<div class="p-5 border-b border-gray-200 bg-white dark:bg-gray-800 dark:border-gray-700">
|
||||
<div class="flex items-center">
|
||||
<div
|
||||
class="w-12 h-12 rounded-full flex items-center justify-center text-white font-bold text-lg mr-4"
|
||||
:style="{ background: chatStore.currentFriend?.color }"
|
||||
>
|
||||
{{ chatStore.currentFriend?.name.charAt(0) }}
|
||||
<div class="chat-header" :class="{ 'dark': themeStore.isDarkMode }">
|
||||
<div class="header-content">
|
||||
<div class="friend-info">
|
||||
<div
|
||||
class="friend-avatar"
|
||||
:style="{ background: chatStore.currentFriend?.color }"
|
||||
>
|
||||
{{ chatStore.currentFriend?.name.charAt(0) }}
|
||||
</div>
|
||||
|
||||
<div class="friend-details">
|
||||
<h3 class="friend-name">{{ chatStore.currentFriend?.name }}</h3>
|
||||
<p class="friend-status">
|
||||
<span
|
||||
class="status-dot"
|
||||
:class="isFriendOnline ? 'online' : 'offline'"
|
||||
></span>
|
||||
<span class="status-text">
|
||||
{{ isFriendOnline ? '在线' : '离线' }}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200">{{ chatStore.currentFriend?.name }}</h3>
|
||||
<p class="text-sm flex items-center gap-1">
|
||||
<span
|
||||
class="w-2 h-2 rounded-full"
|
||||
:class="isFriendOnline ? 'bg-green-500' : 'bg-red-500'"
|
||||
></span>
|
||||
<span :class="isFriendOnline ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'">
|
||||
{{ isFriendOnline ? '在线' : '离线' }}
|
||||
</span>
|
||||
</p>
|
||||
<div class="header-actions">
|
||||
<!-- 语音通话按钮 -->
|
||||
<button
|
||||
class="action-btn voice-call-btn"
|
||||
@click="startVoiceCall"
|
||||
title="语音通话"
|
||||
>
|
||||
<i class="fas fa-phone"></i>
|
||||
</button>
|
||||
|
||||
<!-- 视频通话按钮 -->
|
||||
<button
|
||||
class="action-btn video-call-btn"
|
||||
@click="startVideoCall"
|
||||
title="视频通话"
|
||||
>
|
||||
<i class="fas fa-video"></i>
|
||||
</button>
|
||||
|
||||
<!-- 更多操作 -->
|
||||
<button
|
||||
class="action-btn more-btn"
|
||||
@click="showMoreActions = !showMoreActions"
|
||||
title="更多操作"
|
||||
>
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</button>
|
||||
|
||||
<!-- 更多操作菜单 -->
|
||||
<div v-if="showMoreActions" class="more-actions-menu">
|
||||
<div class="menu-item" @click="clearChatHistory">
|
||||
<i class="fas fa-trash"></i>
|
||||
<span>清空聊天记录</span>
|
||||
</div>
|
||||
<div class="menu-item" @click="viewFriendProfile">
|
||||
<i class="fas fa-user"></i>
|
||||
<span>查看资料</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 视频通话组件 -->
|
||||
<VideoCallComponent
|
||||
v-if="showVideoCall"
|
||||
:caller-info="chatStore.currentFriend"
|
||||
:call-type="callType"
|
||||
:is-incoming="false"
|
||||
@end-call="endCall"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { ref, computed } from 'vue';
|
||||
import { useChatStore } from '@/stores/chat';
|
||||
import { useThemeStore } from '@/stores/theme';
|
||||
import VideoCallComponent from './VideoCallComponent.vue';
|
||||
|
||||
const chatStore = useChatStore();
|
||||
const isFriendOnline = ref(true); // 简化处理,实际应该根据WebSocket状态判断
|
||||
const themeStore = useThemeStore();
|
||||
|
||||
const isFriendOnline = ref(true);
|
||||
const showMoreActions = ref(false);
|
||||
const showVideoCall = ref(false);
|
||||
const callType = ref('video');
|
||||
|
||||
// 点击外部关闭菜单
|
||||
const handleClickOutside = (e) => {
|
||||
if (!e.target.closest('.more-btn') && !e.target.closest('.more-actions-menu')) {
|
||||
showMoreActions.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
|
||||
// 开始语音通话
|
||||
const startVoiceCall = () => {
|
||||
if (!chatStore.currentFriend) {
|
||||
alert('请先选择一个好友');
|
||||
return;
|
||||
}
|
||||
|
||||
callType.value = 'audio';
|
||||
showVideoCall.value = true;
|
||||
console.log('开始语音通话:', chatStore.currentFriend.name);
|
||||
};
|
||||
|
||||
// 开始视频通话
|
||||
const startVideoCall = () => {
|
||||
if (!chatStore.currentFriend) {
|
||||
alert('请先选择一个好友');
|
||||
return;
|
||||
}
|
||||
|
||||
callType.value = 'video';
|
||||
showVideoCall.value = true;
|
||||
console.log('开始视频通话:', chatStore.currentFriend.name);
|
||||
};
|
||||
|
||||
// 结束通话
|
||||
const endCall = () => {
|
||||
showVideoCall.value = false;
|
||||
console.log('通话结束');
|
||||
};
|
||||
|
||||
// 清空聊天记录
|
||||
const clearChatHistory = () => {
|
||||
if (confirm('确定要清空与该好友的聊天记录吗?')) {
|
||||
chatStore.messages = [];
|
||||
showMoreActions.value = false;
|
||||
console.log('聊天记录已清空');
|
||||
}
|
||||
};
|
||||
|
||||
// 查看好友资料
|
||||
const viewFriendProfile = () => {
|
||||
showMoreActions.value = false;
|
||||
console.log('查看好友资料:', chatStore.currentFriend);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chat-header {
|
||||
background: #ffffff;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
padding: 16px 24px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.chat-header.dark {
|
||||
background: #2d2d2d;
|
||||
border-bottom-color: #374151;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.friend-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.friend-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.friend-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.friend-name {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1a202c;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.chat-header.dark .friend-name {
|
||||
color: #f7fafc;
|
||||
}
|
||||
|
||||
.friend-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.status-dot.online {
|
||||
background: #10b981;
|
||||
}
|
||||
|
||||
.status-dot.offline {
|
||||
background: #ef4444;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.chat-header.dark .status-text {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.status-text.online {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.status-text.offline {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 16px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.voice-call-btn {
|
||||
background: linear-gradient(135deg, #10b981, #059669);
|
||||
color: white;
|
||||
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
.voice-call-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(16, 185, 129, 0.4);
|
||||
}
|
||||
|
||||
.video-call-btn {
|
||||
background: linear-gradient(135deg, #3b82f6, #2563eb);
|
||||
color: white;
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.video-call-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(59, 130, 246, 0.4);
|
||||
}
|
||||
|
||||
.more-btn {
|
||||
background: #f1f5f9;
|
||||
color: #64748b;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.more-btn:hover {
|
||||
background: #e2e8f0;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.chat-header.dark .more-btn {
|
||||
background: #374151;
|
||||
color: #9ca3af;
|
||||
border-color: #4b5563;
|
||||
}
|
||||
|
||||
.chat-header.dark .more-btn:hover {
|
||||
background: #4b5563;
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
.more-actions-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
background: white;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
padding: 8px 0;
|
||||
min-width: 160px;
|
||||
z-index: 1000;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.chat-header.dark .more-actions-menu {
|
||||
background: #374151;
|
||||
border-color: #4b5563;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
color: #374151;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.chat-header.dark .menu-item {
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
.chat-header.dark .menu-item:hover {
|
||||
background: #4b5563;
|
||||
}
|
||||
|
||||
.menu-item i {
|
||||
width: 16px;
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.chat-header.dark .menu-item i {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { transform: scale(1); opacity: 1; }
|
||||
50% { transform: scale(1.1); opacity: 0.7; }
|
||||
100% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.chat-header {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.friend-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.friend-name {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,59 +1,35 @@
|
||||
<template>
|
||||
<teleport to="body">
|
||||
<div v-if="visible" class="modal-overlay" @click="handleOverlayClick">
|
||||
<div
|
||||
v-if="visible"
|
||||
class="modal-overlay"
|
||||
:class="{ 'dark': isDark }"
|
||||
@click="handleOverlayClick"
|
||||
class="modal-container"
|
||||
:class="[sizeClass, { 'dark': themeStore.isDarkMode }]"
|
||||
@click.stop
|
||||
>
|
||||
<div
|
||||
class="modal-container"
|
||||
:class="[size, { 'dark': isDark }]"
|
||||
@click.stop
|
||||
>
|
||||
<!-- 模态框头部 -->
|
||||
<div class="modal-header" v-if="showHeader">
|
||||
<h3 class="modal-title">{{ title }}</h3>
|
||||
<button
|
||||
class="modal-close-btn"
|
||||
@click="handleClose"
|
||||
v-if="closable"
|
||||
>
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<!-- 模态框头部 -->
|
||||
<div class="modal-header">
|
||||
<slot name="header">
|
||||
<h2 class="modal-title">{{ title }}</h2>
|
||||
</slot>
|
||||
<button class="close-btn" @click="$emit('close')" v-if="closable">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 模态框内容 -->
|
||||
<div class="modal-body">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<!-- 模态框内容 -->
|
||||
<div class="modal-body">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
<!-- 模态框底部 -->
|
||||
<div class="modal-footer" v-if="showFooter">
|
||||
<slot name="footer">
|
||||
<button
|
||||
class="modal-btn modal-btn-cancel"
|
||||
@click="handleCancel"
|
||||
v-if="showCancel"
|
||||
>
|
||||
{{ cancelText }}
|
||||
</button>
|
||||
<button
|
||||
class="modal-btn modal-btn-confirm"
|
||||
@click="handleConfirm"
|
||||
v-if="showConfirm"
|
||||
>
|
||||
{{ confirmText }}
|
||||
</button>
|
||||
</slot>
|
||||
</div>
|
||||
<!-- 模态框底部 -->
|
||||
<div class="modal-footer" v-if="$slots.footer">
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { computed, onMounted, onUnmounted } from 'vue';
|
||||
import { useThemeStore } from '@/stores/theme';
|
||||
|
||||
const themeStore = useThemeStore();
|
||||
@@ -61,7 +37,7 @@ const themeStore = useThemeStore();
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
default: true
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
@@ -69,7 +45,7 @@ const props = defineProps({
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'medium', // small, medium, large, full
|
||||
default: 'medium',
|
||||
validator: (value) => ['small', 'medium', 'large', 'full'].includes(value)
|
||||
},
|
||||
closable: {
|
||||
@@ -79,55 +55,36 @@ const props = defineProps({
|
||||
maskClosable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showHeader: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showFooter: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showCancel: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showConfirm: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
cancelText: {
|
||||
type: String,
|
||||
default: '取消'
|
||||
},
|
||||
confirmText: {
|
||||
type: String,
|
||||
default: '确定'
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close', 'cancel', 'confirm']);
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
const isDark = computed(() => themeStore.isDarkMode);
|
||||
const sizeClass = computed(() => {
|
||||
return `modal-${props.size}`;
|
||||
});
|
||||
|
||||
const handleOverlayClick = () => {
|
||||
if (props.maskClosable) {
|
||||
handleClose();
|
||||
emit('close');
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
emit('close');
|
||||
const handleEscKey = (event) => {
|
||||
if (event.key === 'Escape' && props.closable) {
|
||||
emit('close');
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
emit('cancel');
|
||||
handleClose();
|
||||
};
|
||||
onMounted(() => {
|
||||
document.addEventListener('keydown', handleEscKey);
|
||||
document.body.style.overflow = 'hidden';
|
||||
});
|
||||
|
||||
const handleConfirm = () => {
|
||||
emit('confirm');
|
||||
};
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('keydown', handleEscKey);
|
||||
document.body.style.overflow = '';
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -137,42 +94,168 @@ const handleConfirm = () => {
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
backdrop-filter: blur(8px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-overlay.dark {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
|
||||
max-height: 90vh;
|
||||
overflow: hidden;
|
||||
animation: slideUp 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
max-height: 90vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
animation: slideUp 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.modal-container.dark {
|
||||
background: #2d2d2d;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
/* 尺寸变体 */
|
||||
.modal-small {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.modal-medium {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.modal-large {
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.modal-full {
|
||||
width: 95vw;
|
||||
height: 95vh;
|
||||
max-width: none;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24px 30px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
background: linear-gradient(135deg, #4361ee, #3f37c9);
|
||||
color: white;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.modal-container.dark .modal-header {
|
||||
border-bottom-color: #4b5563;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 24px 30px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.modal-container.dark .modal-body {
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 20px 30px;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
background: #f8fafc;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.modal-container.dark .modal-footer {
|
||||
background: #374151;
|
||||
border-top-color: #4b5563;
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
:deep(.modal-btn) {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
:deep(.modal-btn-cancel) {
|
||||
background: #f1f5f9;
|
||||
color: #64748b;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
:deep(.modal-btn-cancel:hover) {
|
||||
background: #e2e8f0;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
:deep(.modal-btn-confirm) {
|
||||
background: linear-gradient(135deg, #4361ee, #3f37c9);
|
||||
color: white;
|
||||
}
|
||||
|
||||
:deep(.modal-btn-confirm:hover) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(67, 97, 238, 0.3);
|
||||
}
|
||||
|
||||
.modal-container.dark :deep(.modal-btn-cancel) {
|
||||
background: #4b5563;
|
||||
color: #d1d5db;
|
||||
border-color: #6b7280;
|
||||
}
|
||||
|
||||
.modal-container.dark :deep(.modal-btn-cancel:hover) {
|
||||
background: #6b7280;
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
@@ -186,150 +269,30 @@ const handleConfirm = () => {
|
||||
}
|
||||
}
|
||||
|
||||
.modal-container.small {
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.modal-container.medium {
|
||||
width: 600px;
|
||||
}
|
||||
|
||||
.modal-container.large {
|
||||
width: 800px;
|
||||
}
|
||||
|
||||
.modal-container.full {
|
||||
width: 95vw;
|
||||
height: 95vh;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 24px 24px 16px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.modal-container.dark .modal-header {
|
||||
border-bottom-color: #374151;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #1a202c;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.modal-container.dark .modal-title {
|
||||
color: #f7fafc;
|
||||
}
|
||||
|
||||
.modal-close-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
background: #f7fafc;
|
||||
border-radius: 8px;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.modal-close-btn:hover {
|
||||
background: #e2e8f0;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.modal-container.dark .modal-close-btn {
|
||||
background: #374151;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.modal-container.dark .modal-close-btn:hover {
|
||||
background: #4b5563;
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 24px;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 16px 24px 24px;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.modal-container.dark .modal-footer {
|
||||
border-top-color: #374151;
|
||||
}
|
||||
|
||||
.modal-btn {
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.modal-btn-cancel {
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.modal-btn-cancel:hover {
|
||||
background: #e2e8f0;
|
||||
}
|
||||
|
||||
.modal-btn-confirm {
|
||||
background: linear-gradient(135deg, #4361ee, #3f37c9);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.modal-btn-confirm:hover {
|
||||
background: linear-gradient(135deg, #3f37c9, #3730a3);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.modal-container.dark .modal-btn-cancel {
|
||||
background: #374151;
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
.modal-container.dark .modal-btn-cancel:hover {
|
||||
background: #4b5563;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.modal-container.small,
|
||||
.modal-container.medium,
|
||||
.modal-container.large {
|
||||
width: 95vw;
|
||||
margin: 0 10px;
|
||||
.modal-overlay {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 20px 20px 12px;
|
||||
.modal-small,
|
||||
.modal-medium,
|
||||
.modal-large {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 20px;
|
||||
.modal-full {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.modal-header,
|
||||
.modal-body,
|
||||
.modal-footer {
|
||||
padding: 12px 20px 20px;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -72,6 +72,7 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { useChatStore } from '@/stores/chat';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { useThemeStore } from '@/stores/theme';
|
||||
import CustomInput from './CustomInput.vue';
|
||||
import VirtualList from './VirtualList.vue';
|
||||
@@ -80,6 +81,7 @@ import GroupsManagement from './GroupsManagement.vue';
|
||||
import MomentsView from './MomentsView.vue';
|
||||
|
||||
const chatStore = useChatStore();
|
||||
const userStore = useUserStore();
|
||||
const themeStore = useThemeStore();
|
||||
|
||||
const props = defineProps({
|
||||
@@ -113,8 +115,8 @@ const getListTitle = () => {
|
||||
|
||||
const selectFriend = (friend) => {
|
||||
chatStore.setCurrentFriend(friend);
|
||||
// 清除未读消息
|
||||
friend.unreadCount = 0;
|
||||
// 清除未读消息并加载聊天记录
|
||||
chatStore.switchFriend(friend, userStore.currentUser.id);
|
||||
};
|
||||
|
||||
// 监听当前视图变化,清空搜索
|
||||
@@ -127,7 +129,7 @@ watch(() => props.currentView, () => {
|
||||
.friend-list {
|
||||
width: 320px;
|
||||
min-width: 320px;
|
||||
height: 100%;
|
||||
height: 100vh;
|
||||
background: #ffffff;
|
||||
border-right: 1px solid #e2e8f0;
|
||||
display: flex;
|
||||
@@ -136,7 +138,7 @@ watch(() => props.currentView, () => {
|
||||
}
|
||||
|
||||
.friend-list.dark {
|
||||
background: #2d2d2d;
|
||||
background: #1a1a1a;
|
||||
border-right-color: #374151;
|
||||
}
|
||||
|
||||
@@ -144,10 +146,11 @@ watch(() => props.currentView, () => {
|
||||
padding: 24px 20px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
background: #ffffff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.friend-list.dark .list-header {
|
||||
background: #2d2d2d;
|
||||
background: #1a1a1a;
|
||||
border-bottom-color: #374151;
|
||||
}
|
||||
|
||||
@@ -203,10 +206,13 @@ watch(() => props.currentView, () => {
|
||||
.friends-container {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.friends-list {
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.friend-item {
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
<template>
|
||||
<div class="friends-management h-full bg-gray-50 dark:bg-gray-900">
|
||||
<div class="p-6">
|
||||
<h1 class="text-2xl font-bold text-gray-800 dark:text-gray-200 mb-6">好友管理</h1>
|
||||
<div class="friends-management" :class="{ 'dark': themeStore.isDarkMode }">
|
||||
<div class="management-content">
|
||||
<h1 class="page-title">好友管理</h1>
|
||||
|
||||
<!-- 搜索和筛选 -->
|
||||
<div class="flex gap-4 mb-6">
|
||||
<div class="search-filter-section">
|
||||
<CustomInput
|
||||
v-model="searchQuery"
|
||||
placeholder="搜索好友..."
|
||||
prefix-icon="fas fa-search"
|
||||
clearable
|
||||
class="flex-1"
|
||||
class="search-input"
|
||||
/>
|
||||
<select class="px-4 py-2 border border-gray-300 rounded-lg dark:bg-gray-700 dark:border-gray-600 dark:text-gray-200">
|
||||
<select class="filter-select">
|
||||
<option value="all">全部好友</option>
|
||||
<option value="online">在线好友</option>
|
||||
<option value="offline">离线好友</option>
|
||||
@@ -20,26 +20,26 @@
|
||||
</div>
|
||||
|
||||
<!-- 好友网格 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
<div class="friends-grid">
|
||||
<div
|
||||
v-for="friend in filteredFriends"
|
||||
:key="friend.id"
|
||||
class="bg-white dark:bg-gray-800 rounded-xl p-4 shadow-sm hover:shadow-md transition-all duration-200"
|
||||
class="friend-card"
|
||||
>
|
||||
<div class="text-center">
|
||||
<div class="card-content">
|
||||
<div
|
||||
class="w-16 h-16 rounded-full mx-auto mb-3 flex items-center justify-center text-white font-bold text-xl"
|
||||
class="friend-avatar"
|
||||
:style="{ background: friend.color }"
|
||||
>
|
||||
{{ friend.name.charAt(0) }}
|
||||
</div>
|
||||
<h3 class="font-semibold text-gray-800 dark:text-gray-200 mb-1">{{ friend.name }}</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 mb-3">ID: {{ friend.id }}</p>
|
||||
<div class="flex gap-2 justify-center">
|
||||
<button class="px-3 py-1 bg-blue-500 text-white rounded-lg text-sm hover:bg-blue-600 transition-colors">
|
||||
<h3 class="friend-name">{{ friend.name }}</h3>
|
||||
<p class="friend-id">ID: {{ friend.id }}</p>
|
||||
<div class="friend-actions">
|
||||
<button class="action-btn primary-btn" @click="startChat(friend)">
|
||||
聊天
|
||||
</button>
|
||||
<button class="px-3 py-1 bg-gray-500 text-white rounded-lg text-sm hover:bg-gray-600 transition-colors">
|
||||
<button class="action-btn secondary-btn">
|
||||
详情
|
||||
</button>
|
||||
</div>
|
||||
@@ -53,9 +53,14 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useChatStore } from '@/stores/chat';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { useThemeStore } from '@/stores/theme';
|
||||
import CustomInput from './CustomInput.vue';
|
||||
|
||||
const chatStore = useChatStore();
|
||||
const userStore = useUserStore();
|
||||
const themeStore = useThemeStore();
|
||||
|
||||
const searchQuery = ref('');
|
||||
|
||||
const filteredFriends = computed(() => {
|
||||
@@ -66,4 +71,195 @@ const filteredFriends = computed(() => {
|
||||
friend.id.toString().includes(query)
|
||||
);
|
||||
});
|
||||
|
||||
const startChat = (friend) => {
|
||||
chatStore.setCurrentFriend(friend);
|
||||
chatStore.switchFriend(friend, userStore.currentUser.id);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.friends-management {
|
||||
height: 100%;
|
||||
background: #f8fafc;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.friends-management.dark {
|
||||
background: #111827;
|
||||
}
|
||||
|
||||
.management-content {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #1a202c;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.friends-management.dark .page-title {
|
||||
color: #f7fafc;
|
||||
}
|
||||
|
||||
.search-filter-section {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
padding: 12px 16px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
background: white;
|
||||
color: #1a202c;
|
||||
font-size: 14px;
|
||||
min-width: 140px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.friends-management.dark .filter-select {
|
||||
background: #374151;
|
||||
border-color: #4b5563;
|
||||
color: #f7fafc;
|
||||
}
|
||||
|
||||
.friends-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.friend-card {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.friend-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.friends-management.dark .friend-card {
|
||||
background: #374151;
|
||||
border-color: #4b5563;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.friends-management.dark .friend-card:hover {
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.friend-avatar {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
margin: 0 auto 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 24px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.friend-name {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1a202c;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.friends-management.dark .friend-name {
|
||||
color: #f7fafc;
|
||||
}
|
||||
|
||||
.friend-id {
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
|
||||
.friends-management.dark .friend-id {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.friend-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.primary-btn {
|
||||
background: linear-gradient(135deg, #4361ee, #3f37c9);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.primary-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(67, 97, 238, 0.3);
|
||||
}
|
||||
|
||||
.secondary-btn {
|
||||
background: #f1f5f9;
|
||||
color: #64748b;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.secondary-btn:hover {
|
||||
background: #e2e8f0;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.friends-management.dark .secondary-btn {
|
||||
background: #4b5563;
|
||||
color: #d1d5db;
|
||||
border-color: #6b7280;
|
||||
}
|
||||
|
||||
.friends-management.dark .secondary-btn:hover {
|
||||
background: #6b7280;
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.management-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.search-filter-section {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.friends-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,39 +1,41 @@
|
||||
<template>
|
||||
<div class="groups-management h-full bg-gray-50 dark:bg-gray-900">
|
||||
<div class="p-6">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-800 dark:text-gray-200">群聊管理</h1>
|
||||
<button class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors">
|
||||
<i class="fas fa-plus mr-2"></i>
|
||||
<div class="groups-management" :class="{ 'dark': themeStore.isDarkMode }">
|
||||
<div class="management-content">
|
||||
<div class="header-section">
|
||||
<h1 class="page-title">群聊管理</h1>
|
||||
<button class="create-btn">
|
||||
<i class="fas fa-plus"></i>
|
||||
创建群聊
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 群聊列表 -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
<div class="groups-grid">
|
||||
<div
|
||||
v-for="group in mockGroups"
|
||||
:key="group.id"
|
||||
class="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm hover:shadow-md transition-all duration-200"
|
||||
class="group-card"
|
||||
>
|
||||
<div class="flex items-center mb-4">
|
||||
<div class="w-12 h-12 bg-gradient-to-r from-purple-500 to-pink-500 rounded-xl flex items-center justify-center text-white font-bold text-lg mr-3">
|
||||
{{ group.name.charAt(0) }}
|
||||
<div class="card-content">
|
||||
<div class="group-header">
|
||||
<div class="group-avatar">
|
||||
{{ group.name.charAt(0) }}
|
||||
</div>
|
||||
<div class="group-info">
|
||||
<h3 class="group-name">{{ group.name }}</h3>
|
||||
<p class="group-members">{{ group.memberCount }} 人</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-800 dark:text-gray-200">{{ group.name }}</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">{{ group.memberCount }} 人</p>
|
||||
<p class="group-description">{{ group.description }}</p>
|
||||
<div class="group-actions">
|
||||
<button class="action-btn primary-btn">
|
||||
进入群聊
|
||||
</button>
|
||||
<button class="action-btn secondary-btn">
|
||||
设置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-300 mb-4">{{ group.description }}</p>
|
||||
<div class="flex gap-2">
|
||||
<button class="flex-1 px-3 py-2 bg-blue-500 text-white rounded-lg text-sm hover:bg-blue-600 transition-colors">
|
||||
进入群聊
|
||||
</button>
|
||||
<button class="px-3 py-2 bg-gray-500 text-white rounded-lg text-sm hover:bg-gray-600 transition-colors">
|
||||
设置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -42,6 +44,9 @@
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useThemeStore } from '@/stores/theme';
|
||||
|
||||
const themeStore = useThemeStore();
|
||||
|
||||
const mockGroups = ref([
|
||||
{
|
||||
@@ -64,3 +69,212 @@ const mockGroups = ref([
|
||||
}
|
||||
]);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.groups-management {
|
||||
height: 100%;
|
||||
background: #f8fafc;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.groups-management.dark {
|
||||
background: #111827;
|
||||
}
|
||||
|
||||
.management-content {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.header-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #1a202c;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.groups-management.dark .page-title {
|
||||
color: #f7fafc;
|
||||
}
|
||||
|
||||
.create-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 20px;
|
||||
background: linear-gradient(135deg, #4361ee, #3f37c9);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.create-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(67, 97, 238, 0.3);
|
||||
}
|
||||
|
||||
.groups-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.group-card {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.group-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.groups-management.dark .group-card {
|
||||
background: #374151;
|
||||
border-color: #4b5563;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.groups-management.dark .group-card:hover {
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.group-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.group-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.group-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.group-name {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1a202c;
|
||||
margin: 0 0 4px 0;
|
||||
}
|
||||
|
||||
.groups-management.dark .group-name {
|
||||
color: #f7fafc;
|
||||
}
|
||||
|
||||
.group-members {
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.groups-management.dark .group-members {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.group-description {
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
margin: 0 0 20px 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.groups-management.dark .group-description {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.group-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
padding: 10px 16px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.primary-btn {
|
||||
background: linear-gradient(135deg, #4361ee, #3f37c9);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.primary-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(67, 97, 238, 0.3);
|
||||
}
|
||||
|
||||
.secondary-btn {
|
||||
background: #f1f5f9;
|
||||
color: #64748b;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.secondary-btn:hover {
|
||||
background: #e2e8f0;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.groups-management.dark .secondary-btn {
|
||||
background: #4b5563;
|
||||
color: #d1d5db;
|
||||
border-color: #6b7280;
|
||||
}
|
||||
|
||||
.groups-management.dark .secondary-btn:hover {
|
||||
background: #6b7280;
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.management-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.header-section {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.groups-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,54 +1,80 @@
|
||||
<template>
|
||||
<div class="moments-view h-full bg-gray-50 dark:bg-gray-900">
|
||||
<div class="p-6">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-800 dark:text-gray-200">朋友圈</h1>
|
||||
<button class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors">
|
||||
<i class="fas fa-plus mr-2"></i>
|
||||
发布动态
|
||||
</button>
|
||||
<div class="moments-view" :class="{ 'dark': themeStore.isDarkMode }">
|
||||
<div class="moments-content">
|
||||
<h1 class="page-title">朋友圈</h1>
|
||||
|
||||
<!-- 发布动态 -->
|
||||
<div class="post-composer">
|
||||
<div class="composer-header">
|
||||
<div class="user-avatar" :style="{ background: userStore.currentUser?.color }">
|
||||
{{ userStore.currentUser?.name?.charAt(0) }}
|
||||
</div>
|
||||
<CustomTextarea
|
||||
v-model="newPostContent"
|
||||
placeholder="分享新鲜事..."
|
||||
class="post-input"
|
||||
:rows="3"
|
||||
/>
|
||||
</div>
|
||||
<div class="composer-actions">
|
||||
<div class="media-buttons">
|
||||
<button class="media-btn">
|
||||
<i class="fas fa-image"></i>
|
||||
图片
|
||||
</button>
|
||||
<button class="media-btn">
|
||||
<i class="fas fa-video"></i>
|
||||
视频
|
||||
</button>
|
||||
</div>
|
||||
<button class="publish-btn" :disabled="!newPostContent.trim()">
|
||||
发布
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 动态列表 -->
|
||||
<div class="space-y-6">
|
||||
<div class="moments-list">
|
||||
<div
|
||||
v-for="moment in mockMoments"
|
||||
:key="moment.id"
|
||||
class="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm"
|
||||
class="moment-card"
|
||||
>
|
||||
<div class="flex items-center mb-4">
|
||||
<div
|
||||
class="w-12 h-12 rounded-full flex items-center justify-center text-white font-bold mr-3"
|
||||
:style="{ background: moment.user.color }"
|
||||
>
|
||||
{{ moment.user.name.charAt(0) }}
|
||||
<div class="moment-header">
|
||||
<div class="author-avatar" :style="{ background: moment.author.color }">
|
||||
{{ moment.author.name.charAt(0) }}
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-800 dark:text-gray-200">{{ moment.user.name }}</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">{{ moment.time }}</p>
|
||||
<div class="author-info">
|
||||
<h4 class="author-name">{{ moment.author.name }}</h4>
|
||||
<p class="post-time">{{ moment.time }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-4">{{ moment.content }}</p>
|
||||
<div v-if="moment.images" class="grid grid-cols-3 gap-2 mb-4">
|
||||
<img
|
||||
v-for="(image, index) in moment.images"
|
||||
:key="index"
|
||||
:src="image"
|
||||
class="w-full h-24 object-cover rounded-lg"
|
||||
/>
|
||||
|
||||
<div class="moment-content">
|
||||
<p class="moment-text">{{ moment.content }}</p>
|
||||
<div v-if="moment.images" class="moment-images">
|
||||
<img
|
||||
v-for="(image, index) in moment.images"
|
||||
:key="index"
|
||||
:src="image"
|
||||
:alt="`图片${index + 1}`"
|
||||
class="moment-image"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-4 text-gray-500 dark:text-gray-400">
|
||||
<button class="flex items-center gap-1 hover:text-blue-500 transition-colors">
|
||||
|
||||
<div class="moment-actions">
|
||||
<button class="action-btn like-btn">
|
||||
<i class="fas fa-heart"></i>
|
||||
<span>{{ moment.likes }}</span>
|
||||
{{ moment.likes }}
|
||||
</button>
|
||||
<button class="flex items-center gap-1 hover:text-blue-500 transition-colors">
|
||||
<button class="action-btn comment-btn">
|
||||
<i class="fas fa-comment"></i>
|
||||
<span>{{ moment.comments }}</span>
|
||||
{{ moment.comments }}
|
||||
</button>
|
||||
<button class="flex items-center gap-1 hover:text-blue-500 transition-colors">
|
||||
<button class="action-btn share-btn">
|
||||
<i class="fas fa-share"></i>
|
||||
<span>分享</span>
|
||||
分享
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -59,32 +85,362 @@
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { useThemeStore } from '@/stores/theme';
|
||||
import CustomTextarea from './CustomTextarea.vue';
|
||||
|
||||
const userStore = useUserStore();
|
||||
const themeStore = useThemeStore();
|
||||
|
||||
const newPostContent = ref('');
|
||||
|
||||
const mockMoments = ref([
|
||||
{
|
||||
id: 1,
|
||||
user: { name: '张三', color: '#4cc9f0' },
|
||||
content: '今天天气真不错,适合出去走走!',
|
||||
author: {
|
||||
name: '张三',
|
||||
color: '#ff6b6b'
|
||||
},
|
||||
content: '今天天气真不错,出去走走心情都变好了!',
|
||||
time: '2小时前',
|
||||
likes: 12,
|
||||
comments: 3,
|
||||
images: ['/placeholder.svg?height=100&width=100']
|
||||
images: ['/placeholder.svg?height=200&width=200']
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
user: { name: '李四', color: '#ff6b6b' },
|
||||
content: '刚完成了一个很有挑战性的项目,感觉很有成就感!',
|
||||
author: {
|
||||
name: '李四',
|
||||
color: '#4ecdc4'
|
||||
},
|
||||
content: '刚完成了一个新项目,感觉很有成就感!',
|
||||
time: '5小时前',
|
||||
likes: 25,
|
||||
comments: 8
|
||||
likes: 8,
|
||||
comments: 2
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
user: { name: '王五', color: '#6a0dad' },
|
||||
content: '分享一些学习心得,希望对大家有帮助。',
|
||||
author: {
|
||||
name: '王五',
|
||||
color: '#45b7d1'
|
||||
},
|
||||
content: '分享一张美丽的日落照片',
|
||||
time: '1天前',
|
||||
likes: 18,
|
||||
comments: 5
|
||||
likes: 25,
|
||||
comments: 7,
|
||||
images: ['/placeholder.svg?height=300&width=400']
|
||||
}
|
||||
]);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.moments-view {
|
||||
height: 100%;
|
||||
background: #f8fafc;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.moments-view.dark {
|
||||
background: #111827;
|
||||
}
|
||||
|
||||
.moments-content {
|
||||
padding: 24px;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #1a202c;
|
||||
margin-bottom: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.moments-view.dark .page-title {
|
||||
color: #f7fafc;
|
||||
}
|
||||
|
||||
.post-composer {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.moments-view.dark .post-composer {
|
||||
background: #374151;
|
||||
border-color: #4b5563;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.composer-header {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.post-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.composer-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.media-buttons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.media-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 12px;
|
||||
background: #f1f5f9;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.media-btn:hover {
|
||||
background: #e2e8f0;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.moments-view.dark .media-btn {
|
||||
background: #4b5563;
|
||||
border-color: #6b7280;
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
.moments-view.dark .media-btn:hover {
|
||||
background: #6b7280;
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
.publish-btn {
|
||||
padding: 8px 20px;
|
||||
background: linear-gradient(135deg, #4361ee, #3f37c9);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.publish-btn:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(67, 97, 238, 0.3);
|
||||
}
|
||||
|
||||
.publish-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.moments-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.moment-card {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #e2e8f0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.moment-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.moments-view.dark .moment-card {
|
||||
background: #374151;
|
||||
border-color: #4b5563;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.moments-view.dark .moment-card:hover {
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.moment-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.author-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.author-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.author-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1a202c;
|
||||
margin: 0 0 4px 0;
|
||||
}
|
||||
|
||||
.moments-view.dark .author-name {
|
||||
color: #f7fafc;
|
||||
}
|
||||
|
||||
.post-time {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.moments-view.dark .post-time {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.moment-content {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.moment-text {
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
line-height: 1.6;
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
|
||||
.moments-view.dark .moment-text {
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
.moment-images {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.moment-image {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.moment-image:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.moment-actions {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.moments-view.dark .moment-actions {
|
||||
border-top-color: #4b5563;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 12px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.moments-view.dark .action-btn {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.moments-view.dark .action-btn:hover {
|
||||
background: #4b5563;
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
.like-btn:hover {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.comment-btn:hover {
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.share-btn:hover {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.moments-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.composer-actions {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.media-buttons {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.moment-images {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -29,7 +29,9 @@
|
||||
<div class="nav-icon">
|
||||
<i :class="item.icon"></i>
|
||||
</div>
|
||||
<span class="nav-label">{{ item.label }}</span>
|
||||
<div class="nav-label" :class="{ 'active': activeNav === item.key }">
|
||||
{{ item.label }}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -43,7 +45,7 @@
|
||||
<div class="nav-icon">
|
||||
<i class="fas fa-cog"></i>
|
||||
</div>
|
||||
<span class="nav-label">设置</span>
|
||||
<div class="nav-label">设置</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -198,9 +200,14 @@ const handleNavClick = (key) => {
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
background: linear-gradient(135deg, #ff6b6b, #ee5a52);
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2);
|
||||
box-shadow: 0 8px 20px rgba(255, 107, 107, 0.4);
|
||||
}
|
||||
|
||||
.side-navigation.dark .nav-item.active {
|
||||
background: linear-gradient(135deg, #4cc9f0, #3a9bc1);
|
||||
box-shadow: 0 8px 20px rgba(76, 201, 240, 0.4);
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
@@ -218,18 +225,34 @@ const handleNavClick = (key) => {
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
opacity: 0.9;
|
||||
opacity: 0.8;
|
||||
transition: all 0.3s ease;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
padding: 4px 8px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
backdrop-filter: blur(5px);
|
||||
min-width: 32px;
|
||||
}
|
||||
|
||||
.nav-label.active {
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0.2));
|
||||
border-color: rgba(255, 255, 255, 0.4);
|
||||
opacity: 1;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.side-navigation.dark .nav-label.active {
|
||||
background: linear-gradient(135deg, rgba(76, 201, 240, 0.3), rgba(58, 155, 193, 0.2));
|
||||
border-color: rgba(76, 201, 240, 0.4);
|
||||
}
|
||||
|
||||
.nav-item:hover .nav-label {
|
||||
opacity: 1;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.nav-item.active .nav-label {
|
||||
opacity: 1;
|
||||
font-weight: 600;
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.bottom-actions {
|
||||
@@ -268,6 +291,8 @@ const handleNavClick = (key) => {
|
||||
|
||||
.nav-label {
|
||||
font-size: 9px;
|
||||
padding: 3px 6px;
|
||||
min-width: 28px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -58,11 +58,12 @@
|
||||
{{ callerInfo.name.charAt(0) }}
|
||||
</div>
|
||||
<div class="placeholder-text">{{ callerInfo.name }}</div>
|
||||
<div class="connection-status">{{ connectionText }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 本地视频 -->
|
||||
<div class="local-video-container" v-if="hasLocalVideo">
|
||||
<div class="local-video-container" v-if="hasLocalVideo && callType === 'video'">
|
||||
<video
|
||||
ref="localVideo"
|
||||
class="local-video"
|
||||
@@ -77,7 +78,7 @@
|
||||
<div v-if="!isMinimized" class="call-controls">
|
||||
<button
|
||||
class="control-btn"
|
||||
:class="{ 'active': isMicOn }"
|
||||
:class="{ 'active': isMicOn, 'disabled': !isMicOn }"
|
||||
@click="toggleMic"
|
||||
:title="isMicOn ? '关闭麦克风' : '开启麦克风'"
|
||||
>
|
||||
@@ -85,8 +86,9 @@
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="callType === 'video'"
|
||||
class="control-btn"
|
||||
:class="{ 'active': isCameraOn }"
|
||||
:class="{ 'active': isCameraOn, 'disabled': !isCameraOn }"
|
||||
@click="toggleCamera"
|
||||
:title="isCameraOn ? '关闭摄像头' : '开启摄像头'"
|
||||
>
|
||||
@@ -95,6 +97,7 @@
|
||||
|
||||
<button
|
||||
class="control-btn speaker-btn"
|
||||
:class="{ 'active': isSpeakerOn }"
|
||||
@click="toggleSpeaker"
|
||||
:title="isSpeakerOn ? '关闭扬声器' : '开启扬声器'"
|
||||
>
|
||||
@@ -125,7 +128,7 @@ const props = defineProps({
|
||||
},
|
||||
callType: {
|
||||
type: String,
|
||||
default: 'video', // 'video' or 'audio'
|
||||
default: 'video',
|
||||
validator: (value) => ['video', 'audio'].includes(value)
|
||||
},
|
||||
isIncoming: {
|
||||
@@ -144,6 +147,7 @@ const isCameraOn = ref(props.callType === 'video');
|
||||
const isSpeakerOn = ref(true);
|
||||
const hasLocalVideo = ref(false);
|
||||
const hasRemoteVideo = ref(false);
|
||||
const connectionText = ref('正在连接...');
|
||||
|
||||
// 拖拽相关
|
||||
const position = reactive({ x: 100, y: 100 });
|
||||
@@ -163,7 +167,7 @@ const callStatus = computed(() => {
|
||||
if (props.isIncoming) {
|
||||
return '来电中...';
|
||||
}
|
||||
return '通话中';
|
||||
return '通话中 00:32';
|
||||
});
|
||||
|
||||
// 初始化媒体设备
|
||||
@@ -187,11 +191,24 @@ const initializeMedia = async () => {
|
||||
isCameraOn.value = false;
|
||||
}
|
||||
|
||||
// 模拟连接成功
|
||||
setTimeout(() => {
|
||||
connectionText.value = '已连接';
|
||||
// 模拟远程视频(实际应用中这里会是真实的远程流)
|
||||
if (props.callType === 'video') {
|
||||
hasRemoteVideo.value = false; // 设为false显示头像
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('无法访问媒体设备:', error);
|
||||
isCameraOn.value = false;
|
||||
connectionText.value = '连接失败';
|
||||
|
||||
if (error.name === 'NotAllowedError') {
|
||||
alert('请允许访问摄像头和麦克风');
|
||||
} else if (error.name === 'NotFoundError') {
|
||||
alert('未找到摄像头或麦克风设备');
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -208,7 +225,7 @@ const toggleMic = () => {
|
||||
};
|
||||
|
||||
const toggleCamera = () => {
|
||||
if (localStream.value) {
|
||||
if (localStream.value && props.callType === 'video') {
|
||||
const videoTracks = localStream.value.getVideoTracks();
|
||||
videoTracks.forEach(track => {
|
||||
track.enabled = !track.enabled;
|
||||
@@ -249,6 +266,7 @@ const startDrag = (event) => {
|
||||
|
||||
document.addEventListener('mousemove', handleDrag);
|
||||
document.addEventListener('mouseup', stopDrag);
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
const handleDrag = (event) => {
|
||||
@@ -448,6 +466,8 @@ onUnmounted(() => {
|
||||
flex: 1;
|
||||
background: #000;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.remote-video-container {
|
||||
@@ -471,6 +491,7 @@ onUnmounted(() => {
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
color: white;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.placeholder-avatar {
|
||||
@@ -488,6 +509,12 @@ onUnmounted(() => {
|
||||
.placeholder-text {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.connection-status {
|
||||
font-size: 14px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.local-video-container {
|
||||
@@ -541,6 +568,10 @@ onUnmounted(() => {
|
||||
background: #4caf50;
|
||||
}
|
||||
|
||||
.control-btn.disabled {
|
||||
background: #ff6b6b;
|
||||
}
|
||||
|
||||
.end-call-btn {
|
||||
background: #ff6b6b;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ export function useWebSocket() {
|
||||
try {
|
||||
chatStore.connectionStatus = "connecting"
|
||||
|
||||
const wsUrl = "ws://g-ws.nailaoyun.cn/ws"
|
||||
// const wsUrl = "ws://g-ws.nailaoyun.cn/ws"
|
||||
const wsUrl = "ws://127.0.0.1:12080/ws"
|
||||
console.log("连接WebSocket:", wsUrl)
|
||||
|
||||
chatStore.socket = new WebSocket(wsUrl)
|
||||
|
||||
@@ -84,6 +84,15 @@ export const useChatStore = defineStore("chat", () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 设置当前好友 - 新增方法
|
||||
const setCurrentFriend = (friend) => {
|
||||
currentFriend.value = friend
|
||||
// 清除未读消息计数
|
||||
if (friend) {
|
||||
friend.unreadCount = 0
|
||||
}
|
||||
}
|
||||
|
||||
// 切换当前聊天好友
|
||||
const switchFriend = async (friend, currentUserId) => {
|
||||
currentFriend.value = friend
|
||||
@@ -101,27 +110,6 @@ export const useChatStore = defineStore("chat", () => {
|
||||
messages.value = []
|
||||
}
|
||||
}
|
||||
// 切换当前聊天好友
|
||||
const setCurrentFriend = async (friend, currentUserId) => {
|
||||
currentFriend.value = friend
|
||||
|
||||
// 清空未读消息计数
|
||||
friend.unreadCount = 0
|
||||
await chatDB.clearUnreadCount(friend.id)
|
||||
|
||||
// 加载聊天记录
|
||||
try {
|
||||
const chatHistory = await chatDB.getChatHistory(currentUserId, friend.id)
|
||||
console.log("chatHistory:", chatHistory)
|
||||
console.log("currentUserId:", currentUserId)
|
||||
console.log("friend.id:", friend.id)
|
||||
messages.value = chatHistory.sort((a, b) => a.timestamp - b.timestamp)
|
||||
console.log("messages.value:", messages.value)
|
||||
} catch (error) {
|
||||
console.error("加载聊天记录失败:", error)
|
||||
messages.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 添加消息
|
||||
const addMessage = async (message, currentUserId) => {
|
||||
@@ -215,8 +203,8 @@ export const useChatStore = defineStore("chat", () => {
|
||||
connectionStatusIcon,
|
||||
initDB,
|
||||
loadFriends,
|
||||
switchFriend,
|
||||
setCurrentFriend,
|
||||
switchFriend,
|
||||
addMessage,
|
||||
handleIncomingMessage,
|
||||
}
|
||||
|
||||
@@ -27,6 +27,10 @@
|
||||
--message-text-received: #212529;
|
||||
--input-bg: #ffffff;
|
||||
--header-bg: #ffffff;
|
||||
|
||||
/* 亮色模式背景装饰 */
|
||||
--bg-pattern: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" opacity="0.03"><rect width="100" height="100" fill="none"/><circle cx="50" cy="50" r="2" fill="%234361ee"/></svg>');
|
||||
--chat-bg: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
@@ -44,6 +48,10 @@
|
||||
--message-text-received: #ffffff;
|
||||
--input-bg: #2d2d2d;
|
||||
--header-bg: #2d2d2d;
|
||||
|
||||
/* 暗色模式背景装饰 */
|
||||
--bg-pattern: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" opacity="0.05"><rect width="100" height="100" fill="none"/><path d="M0,0 L100,100 M100,0 L0,100" stroke="%234cc9f0" stroke-width="0.5"/></svg>');
|
||||
--chat-bg: linear-gradient(135deg, #111827 0%, #1f2937 100%);
|
||||
}
|
||||
|
||||
/* 全局样式 */
|
||||
@@ -56,6 +64,30 @@ body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
transition: all 0.3s ease;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* 主应用背景 */
|
||||
#app {
|
||||
background: var(--chat-bg);
|
||||
background-image: var(--bg-pattern);
|
||||
background-size: 200px 200px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* 聊天区域背景 */
|
||||
.chat-area {
|
||||
background: var(--bg-primary);
|
||||
background-image: var(--bg-pattern);
|
||||
background-size: 150px 150px;
|
||||
}
|
||||
|
||||
/* 消息列表背景 */
|
||||
.message-list-background {
|
||||
background: var(--bg-secondary);
|
||||
background-image: var(--bg-pattern);
|
||||
background-size: 100px 100px;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
@@ -184,6 +216,24 @@ body {
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
|
||||
/* 渐变动画 */
|
||||
@keyframes gradient-shift {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.gradient-animated {
|
||||
background-size: 200% 200%;
|
||||
animation: gradient-shift 3s ease infinite;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 1024px) {
|
||||
.chat-container {
|
||||
@@ -202,4 +252,47 @@ body {
|
||||
.chat-area {
|
||||
min-width: 400px;
|
||||
}
|
||||
|
||||
#app {
|
||||
background-size: 150px 150px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 自定义组件样式 */
|
||||
.custom-input,
|
||||
.custom-textarea {
|
||||
background: var(--input-bg);
|
||||
border-color: var(--border-color);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.custom-input:focus,
|
||||
.custom-textarea:focus {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 2px rgba(67, 97, 238, 0.2);
|
||||
}
|
||||
|
||||
/* 卡片样式 */
|
||||
.card {
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
box-shadow: 0 2px 8px var(--shadow);
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--primary), var(--secondary));
|
||||
color: white;
|
||||
border: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(67, 97, 238, 0.3);
|
||||
}
|
||||
|
||||
/* 主题切换动画 */
|
||||
* {
|
||||
transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
@@ -19,8 +19,8 @@ export default defineConfig({
|
||||
proxy: {
|
||||
'/api': {
|
||||
// target: 'http://127.0.0.1:18007/api/',
|
||||
// target: 'http://127.0.0.1:12080',
|
||||
target: 'http://g-ws.nailaoyun.cn',
|
||||
target: 'http://127.0.0.1:12080',
|
||||
// target: 'http://g-ws.nailaoyun.cn',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user