更新了一些功能(媒体预览有bug)

This commit is contained in:
2025-07-03 17:02:08 +08:00
parent bcee1e7cc6
commit 90d6f01b50
12 changed files with 1618 additions and 386 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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)

View File

@@ -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,
}

View File

@@ -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;
}

View File

@@ -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/, '')
}