diff --git a/frontend/src/views/chat/ChatRoomView.vue b/frontend/src/views/chat/ChatRoomView.vue index 1ca288a..23dc4fe 100644 --- a/frontend/src/views/chat/ChatRoomView.vue +++ b/frontend/src/views/chat/ChatRoomView.vue @@ -2,22 +2,22 @@
- + +
+ + {{ (conversationName || '群')[0] }} + +
{{ conversationName }} - ({{ convDetail.members?.length || 0 }}人) + {{ convDetail.members?.length || 0 }} 位成员 + 在线
- - - 经典 - 紧凑 - 气泡 - - - ℹ️ + + 群信息 ▾
@@ -25,41 +25,44 @@
-

开始聊天吧 🌿

+
🌿
+

开始聊天吧

+

发送第一条消息

+ +
+ {{ formatTimeDivider(msg.created_at) }} +
+
@@ -77,10 +80,14 @@
+
+ 😊 + 🖼️ +
- - ➤ + + 发送
@@ -150,45 +157,111 @@ function sendMessage() { }) } +function avatarColor(name: string) { + const colors = ['#009688', '#26A69A', '#00796B', '#00897B', '#4DB6AC', '#00838F', '#00695C'] + const idx = (name || '').charCodeAt(0) % colors.length + return colors[idx] +} + +function shouldShowTime(msg: any, idx: number) { + if (idx === 0) return true + const prev = chatStore.currentMessages[idx - 1] + if (!prev) return false + return dayjs(msg.created_at).diff(dayjs(prev.created_at), 'minute') > 5 +} + function formatTime(time: string) { return dayjs(time).format('HH:mm') } + +function formatTimeDivider(time: string) { + const d = dayjs(time) + const now = dayjs() + if (d.isSame(now, 'day')) return d.format('HH:mm') + if (d.isSame(now.subtract(1, 'day'), 'day')) return '昨天 ' + d.format('HH:mm') + return d.format('MM月DD日 HH:mm') +} diff --git a/frontend/src/views/chat/ConversationListPanel.vue b/frontend/src/views/chat/ConversationListPanel.vue index 963c172..82b3704 100644 --- a/frontend/src/views/chat/ConversationListPanel.vue +++ b/frontend/src/views/chat/ConversationListPanel.vue @@ -11,76 +11,43 @@ - -
- - 列表 - 卡片 - 瀑布流 - -
-
加载中...
-
-
💬
-

{{ searchKeyword ? '没有找到匹配的会话' : '暂无消息' }}

-

去通讯录找朋友聊天吧

+
+
+ 加载中...
- - - - - - +
@@ -91,13 +58,11 @@ import { ref, computed } from 'vue' import { useRouter } from 'vue-router' import { useChatStore } from '@/stores/chat' -import { useUiStore } from '@/stores/ui' import CreateGroupModal from './CreateGroupModal.vue' import dayjs from 'dayjs' const router = useRouter() const chatStore = useChatStore() -const uiStore = useUiStore() const searchKeyword = ref('') const showCreateGroup = ref(false) @@ -110,6 +75,12 @@ const filteredConversations = computed(() => { ) }) +function avatarStyle(conv: any) { + const colors = ['#009688', '#26A69A', '#00796B', '#00897B', '#4DB6AC'] + const idx = (conv.name || '').charCodeAt(0) % colors.length + return { background: colors[idx] } +} + function openChat(id: string) { router.push(`/chat/${id}`) } @@ -124,6 +95,7 @@ function formatTime(time: string | null) { const now = dayjs() if (d.isSame(now, 'day')) return d.format('HH:mm') if (d.isSame(now.subtract(1, 'day'), 'day')) return '昨天' + if (now.diff(d, 'day') < 7) return `${now.diff(d, 'day')}天前` return d.format('MM/DD') } @@ -142,6 +114,7 @@ function formatTime(time: string | null) { padding: 16px; border-bottom: 1px solid var(--color-border); } +.header-actions { display: flex; gap: 4px; } .panel-title { margin: 0; @@ -150,67 +123,73 @@ function formatTime(time: string | null) { color: var(--color-text-primary); } -.search-bar { - padding: 8px 12px; -} +.search-bar { padding: 8px 12px; } -.layout-switch { - padding: 4px 12px 8px; - text-align: center; -} +.conversation-list { flex: 1; overflow-y: auto; } -.conversation-list { - flex: 1; - overflow-y: auto; +/* 加载状态 */ +.loading-state { + display: flex; flex-direction: column; align-items: center; + gap: 8px; padding: 60px 20px; color: var(--color-text-hint); } - -.empty-state { - text-align: center; - padding: 60px 20px; - color: var(--color-text-secondary); +.loading-spinner { + width: 28px; height: 28px; border: 3px solid var(--color-border); + border-top-color: var(--color-primary); border-radius: 50%; + animation: spin 0.8s linear infinite; } +@keyframes spin { to { transform: rotate(360deg); } } -/* 列表模式 */ +/* 空状态 */ +.empty-state { text-align: center; padding: 60px 20px; } +.empty-icon { font-size: 48px; margin-bottom: 8px; } +.empty-title { color: var(--color-text-secondary); font-size: 15px; margin: 0; } +.empty-hint { color: var(--color-text-hint); font-size: 13px; margin-top: 4px; } + +/* 会话列表项 */ .conv-item { - display: flex; - align-items: center; - padding: 12px 16px; - gap: 12px; - cursor: pointer; - transition: background 0.15s; - border-bottom: 0.5px solid var(--color-border); + display: flex; align-items: center; padding: 12px 16px; gap: 12px; + cursor: pointer; transition: all 0.2s; position: relative; } .conv-item:hover { background: var(--color-primary-lightest); } -.conv-item.active { background: var(--color-primary-lightest); border-left: 3px solid var(--color-primary); } +.conv-item.active { + background: var(--color-primary-lightest); +} +.conv-item.active::before { + content: ''; position: absolute; left: 0; top: 50%; + transform: translateY(-50%); width: 3px; height: 60%; + background: var(--color-primary); border-radius: 0 2px 2px 0; +} + +.conv-avatar-wrap { position: relative; flex-shrink: 0; } +.conv-type-badge { + position: absolute; bottom: -2px; right: -2px; + background: #FF9800; color: white; font-size: 9px; + padding: 0 4px; border-radius: 6px; line-height: 14px; font-weight: 600; +} +.unread-dot { + position: absolute; top: 0; right: 0; + width: 10px; height: 10px; background: #EF5350; + border-radius: 50%; border: 2px solid var(--color-surface); +} + .conv-info { flex: 1; min-width: 0; } -.conv-top { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; } -.conv-name { font-weight: 500; font-size: 14px; } -.conv-time { font-size: 11px; color: var(--color-text-hint); } +.conv-top { + display: flex; justify-content: space-between; align-items: center; + margin-bottom: 4px; +} +.conv-name { font-weight: 500; font-size: 14px; color: var(--color-text-primary); } +.conv-time { font-size: 11px; color: var(--color-text-hint); flex-shrink: 0; } + .conv-bottom { display: flex; justify-content: space-between; align-items: center; } -.conv-preview { font-size: 12px; color: var(--color-text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; } - -/* 卡片模式 */ -.card-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; padding: 8px 12px; } -.conv-card { - background: var(--color-surface-elevated); border-radius: 10px; padding: 14px; - text-align: center; cursor: pointer; border: 1px solid var(--color-border); - transition: all 0.2s; position: relative; +.conv-preview { + font-size: 12px; color: var(--color-text-secondary); + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; } -.conv-card:hover { border-color: var(--color-primary-lighter); box-shadow: 0 2px 8px rgba(0,150,136,0.08); } -.conv-card.active { border-color: var(--color-primary); background: var(--color-primary-lightest); } -.conv-card-name { font-weight: 500; margin-top: 6px; font-size: 13px; } -.conv-card-preview { font-size: 11px; color: var(--color-text-hint); margin-top: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -/* 瀑布流模式 */ -.waterfall-grid { columns: 2; column-gap: 8px; padding: 8px 12px; } -.conv-waterfall { - break-inside: avoid; background: var(--color-surface-elevated); border-radius: 10px; - padding: 12px; margin-bottom: 8px; cursor: pointer; border: 1px solid var(--color-border); - transition: all 0.2s; +.unread-badge { + background: #EF5350; color: white; font-size: 11px; font-weight: 600; + min-width: 18px; height: 18px; border-radius: 9px; + display: flex; align-items: center; justify-content: center; + padding: 0 5px; flex-shrink: 0; margin-left: 6px; } -.conv-waterfall:hover { border-color: var(--color-primary-lighter); } -.wf-header { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; } -.wf-name { font-weight: 500; font-size: 13px; } -.wf-content { font-size: 12px; color: var(--color-text-secondary); line-height: 1.4; margin-bottom: 6px; } -.wf-footer { display: flex; justify-content: space-between; align-items: center; font-size: 11px; color: var(--color-text-hint); }