-
+
{{ msg.sender_name }}
{{ msg.content }}
-
- {{ formatTime(msg.created_at) }}
+
+ {{ formatTime(msg.created_at) }}
-
-
我
+
+
+ {{ (auth.user?.nickname || auth.user?.username || '我')[0] }}
+
@@ -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 ? '没有找到匹配的会话' : '暂无消息' }}
-
去通讯录找朋友聊天吧
+
-
-
+
+
💬
+
{{ searchKeyword ? '没有找到匹配的会话' : '暂无消息' }}
+
去通讯录找朋友聊天吧
+
+ 添加好友
+
+
+
-
- {{ (conv.name || '?')[0] }}
-
+
+
+ {{ (conv.name || '?')[0] }}
+
+ 群
+
+
{{ conv.name || '未命名' }}
{{ formatTime(conv.last_message_at) }}
- {{ conv.last_message_preview || '' }}
-
+ {{ conv.last_message_preview || '暂无消息' }}
+ {{ conv.unread_count > 99 ? '99+' : conv.unread_count }}
-
-
-
-
-
-
- {{ (conv.name || '?')[0] }}
-
-
{{ conv.name || '未命名' }}
-
{{ conv.last_message_preview?.substring(0, 30) || '' }}
-
-
-
-
-
-
-
-
-
-
{{ conv.last_message_preview || '暂无消息' }}
-
-
-
-
+
@@ -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); }