1.1
This commit is contained in:
@@ -2,22 +2,22 @@
|
||||
<div class="chat-room">
|
||||
<!-- 聊天头部 -->
|
||||
<div class="chat-header">
|
||||
<n-button v-if="uiStore.isMobile" quaternary circle @click="$router.push('/chat')">←</n-button>
|
||||
<n-button v-if="uiStore.isMobile" quaternary circle @click="$router.push('/chat')" size="small">←</n-button>
|
||||
<div class="header-avatar" v-if="convDetail?.type === 'group'">
|
||||
<n-avatar :size="36" round :style="{ background: 'var(--color-primary)' }">
|
||||
{{ (conversationName || '群')[0] }}
|
||||
</n-avatar>
|
||||
</div>
|
||||
<div class="header-info">
|
||||
<span class="room-name">{{ conversationName }}</span>
|
||||
<span v-if="convDetail?.type === 'group'" class="member-count">
|
||||
({{ convDetail.members?.length || 0 }}人)
|
||||
{{ convDetail.members?.length || 0 }} 位成员
|
||||
</span>
|
||||
<span v-else class="member-count online-status">在线</span>
|
||||
</div>
|
||||
<!-- 聊天风格切换 -->
|
||||
<n-button-group size="tiny" style="margin-left: auto">
|
||||
<n-button :type="uiStore.chatStyle === 'classic' ? 'primary' : 'default'" @click="uiStore.setChatStyle('classic')">经典</n-button>
|
||||
<n-button :type="uiStore.chatStyle === 'compact' ? 'primary' : 'default'" @click="uiStore.setChatStyle('compact')">紧凑</n-button>
|
||||
<n-button :type="uiStore.chatStyle === 'bubble' ? 'primary' : 'default'" @click="uiStore.setChatStyle('bubble')">气泡</n-button>
|
||||
</n-button-group>
|
||||
<!-- 群聊信息按钮 -->
|
||||
<n-button v-if="convDetail?.type === 'group'" quaternary circle size="small" @click="showGroupInfo = !showGroupInfo" title="群信息">
|
||||
ℹ️
|
||||
<n-button v-if="convDetail?.type === 'group'" quaternary size="small" @click="showGroupInfo = !showGroupInfo" title="群信息">
|
||||
群信息 ▾
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
@@ -25,41 +25,44 @@
|
||||
<!-- 消息列表 -->
|
||||
<div class="message-list" ref="messageListRef">
|
||||
<div v-if="chatStore.currentMessages.length === 0" class="no-messages">
|
||||
<p>开始聊天吧 🌿</p>
|
||||
<div class="no-msg-icon">🌿</div>
|
||||
<p class="no-msg-title">开始聊天吧</p>
|
||||
<p class="no-msg-hint">发送第一条消息</p>
|
||||
</div>
|
||||
<div
|
||||
v-for="msg in chatStore.currentMessages" :key="msg.id"
|
||||
class="message-row" :class="{
|
||||
'own': msg.sender_id === auth.user?.id,
|
||||
'other': msg.sender_id !== auth.user?.id,
|
||||
[`style-${uiStore.chatStyle}`]: true,
|
||||
}"
|
||||
v-for="(msg, idx) in chatStore.currentMessages" :key="msg.id"
|
||||
class="message-row" :class="{ 'own': msg.sender_id === auth.user?.id, 'other': msg.sender_id !== auth.user?.id }"
|
||||
>
|
||||
<!-- 时间分隔线 -->
|
||||
<div v-if="shouldShowTime(msg, idx)" class="time-divider">
|
||||
{{ formatTimeDivider(msg.created_at) }}
|
||||
</div>
|
||||
|
||||
<template v-if="msg.type === 'system'">
|
||||
<div class="system-msg">{{ msg.content }}</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div v-if="msg.sender_id !== auth.user?.id" class="avatar">
|
||||
<n-avatar :size="uiStore.chatStyle === 'compact' ? 28 : 34" round
|
||||
:style="{ background: 'var(--color-primary)' }">
|
||||
<div v-if="msg.sender_id !== auth.user?.id" class="avatar-wrap">
|
||||
<n-avatar :size="36" round :style="{ background: avatarColor(msg.sender_name) }">
|
||||
{{ (msg.sender_name || '?')[0] }}
|
||||
</n-avatar>
|
||||
</div>
|
||||
<div class="bubble-area">
|
||||
<div v-if="uiStore.chatStyle !== 'compact' && msg.sender_id !== auth.user?.id" class="sender-name">
|
||||
<div v-if="msg.sender_id !== auth.user?.id && convDetail?.type === 'group'" class="sender-name">
|
||||
{{ msg.sender_name }}
|
||||
</div>
|
||||
<div class="bubble" :class="{ 'bubble-self': msg.sender_id === auth.user?.id, 'bubble-other': msg.sender_id !== auth.user?.id }">
|
||||
<img v-if="msg.type === 'image'" :src="msg.content" class="msg-image" />
|
||||
<span v-else>{{ msg.content }}</span>
|
||||
</div>
|
||||
<div v-if="uiStore.chatStyle === 'classic'" class="msg-time">
|
||||
{{ formatTime(msg.created_at) }}
|
||||
<div class="msg-meta">
|
||||
<span class="msg-time">{{ formatTime(msg.created_at) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="msg.sender_id === auth.user?.id" class="avatar">
|
||||
<n-avatar :size="uiStore.chatStyle === 'compact' ? 28 : 34" round
|
||||
style="background: var(--color-primary-dark)">我</n-avatar>
|
||||
<div v-if="msg.sender_id === auth.user?.id" class="avatar-wrap">
|
||||
<n-avatar :size="36" round :style="{ background: 'var(--color-primary-dark)' }">
|
||||
{{ (auth.user?.nickname || auth.user?.username || '我')[0] }}
|
||||
</n-avatar>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -77,10 +80,14 @@
|
||||
|
||||
<!-- 输入框 -->
|
||||
<div class="input-bar">
|
||||
<div class="input-actions">
|
||||
<span class="action-icon" title="表情">😊</span>
|
||||
<span class="action-icon" title="图片">🖼️</span>
|
||||
</div>
|
||||
<n-input v-model:value="inputText" type="textarea" :autosize="{ minRows: 1, maxRows: 4 }"
|
||||
placeholder="输入消息..." @keydown.enter.exact.prevent="sendMessage" />
|
||||
<n-button type="primary" :disabled="!inputText.trim()" @click="sendMessage" circle>
|
||||
➤
|
||||
<n-button type="primary" :disabled="!inputText.trim()" @click="sendMessage" round size="small">
|
||||
发送
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chat-room { display: flex; flex-direction: column; height: 100%; }
|
||||
.chat-room { display: flex; flex-direction: column; height: 100%; background: var(--color-bg); }
|
||||
|
||||
.chat-header {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
padding: 12px 20px; border-bottom: 1px solid var(--color-border);
|
||||
background: var(--color-surface);
|
||||
}
|
||||
.header-info { display: flex; align-items: baseline; gap: 4px; }
|
||||
.room-name { font-weight: 600; font-size: 16px; }
|
||||
.header-info { flex: 1; display: flex; flex-direction: column; gap: 1px; }
|
||||
.room-name { font-weight: 600; font-size: 16px; color: var(--color-text-primary); }
|
||||
.member-count { font-size: 12px; color: var(--color-text-hint); }
|
||||
.online-status { color: var(--color-success); }
|
||||
.online-status::before {
|
||||
content: ''; display: inline-block; width: 6px; height: 6px;
|
||||
background: var(--color-success); border-radius: 50%; margin-right: 4px;
|
||||
}
|
||||
|
||||
.chat-body { flex: 1; display: flex; overflow: hidden; }
|
||||
|
||||
.message-list { flex: 1; overflow-y: auto; padding: 16px 20px; }
|
||||
.no-messages { text-align: center; padding-top: 120px; color: var(--color-text-hint); }
|
||||
|
||||
.message-row { display: flex; align-items: flex-start; margin-bottom: 12px; gap: 8px; }
|
||||
.no-messages { text-align: center; padding-top: 100px; }
|
||||
.no-msg-icon { font-size: 48px; margin-bottom: 8px; }
|
||||
.no-msg-title { color: var(--color-text-secondary); font-size: 16px; margin: 0; }
|
||||
.no-msg-hint { color: var(--color-text-hint); font-size: 13px; margin-top: 4px; }
|
||||
|
||||
/* 时间分隔线 */
|
||||
.time-divider {
|
||||
text-align: center; margin: 16px 0 8px;
|
||||
font-size: 11px; color: var(--color-text-hint);
|
||||
}
|
||||
.time-divider::before, .time-divider::after {
|
||||
content: ''; display: inline-block; width: 40px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
vertical-align: middle; margin: 0 8px;
|
||||
}
|
||||
|
||||
.message-row { display: flex; align-items: flex-start; margin-bottom: 16px; gap: 8px; }
|
||||
.message-row.own { justify-content: flex-end; }
|
||||
.message-row.other { justify-content: flex-start; }
|
||||
.message-row.style-compact { margin-bottom: 4px; }
|
||||
|
||||
.bubble-area { max-width: 65%; }
|
||||
.sender-name { font-size: 12px; color: var(--color-text-hint); margin-bottom: 2px; margin-left: 4px; }
|
||||
.bubble { padding: 10px 14px; border-radius: 12px; font-size: 15px; line-height: 1.5; word-break: break-word; }
|
||||
.bubble-self { background: var(--color-bubble-self); color: var(--color-bubble-self-text); border-top-right-radius: 4px; }
|
||||
.bubble-other { background: var(--color-bubble-other); color: var(--color-bubble-other-text); border-top-left-radius: 4px; }
|
||||
.msg-time { font-size: 11px; color: var(--color-text-hint); margin-top: 2px; }
|
||||
.msg-image { max-width: 200px; border-radius: 8px; }
|
||||
.avatar-wrap { flex-shrink: 0; }
|
||||
|
||||
.system-msg { text-align: center; font-size: 12px; color: var(--color-text-hint); margin: 8px 0; padding: 4px 12px; }
|
||||
.bubble-area { max-width: 60%; }
|
||||
.sender-name { font-size: 12px; color: var(--color-text-secondary); margin-bottom: 3px; margin-left: 4px; font-weight: 500; }
|
||||
|
||||
.bubble {
|
||||
padding: 10px 14px; font-size: 15px; line-height: 1.6; word-break: break-word;
|
||||
position: relative;
|
||||
}
|
||||
.bubble-self {
|
||||
background: var(--color-bubble-self); color: var(--color-bubble-self-text);
|
||||
border-radius: 16px 16px 4px 16px;
|
||||
box-shadow: 0 1px 2px rgba(0,150,136,0.15);
|
||||
}
|
||||
.bubble-other {
|
||||
background: var(--color-bubble-other); color: var(--color-bubble-other-text);
|
||||
border-radius: 16px 16px 16px 4px;
|
||||
}
|
||||
.msg-meta { margin-top: 2px; }
|
||||
.msg-time { font-size: 10px; color: var(--color-text-hint); margin-left: 4px; }
|
||||
.own .msg-meta { text-align: right; margin-right: 4px; }
|
||||
|
||||
.msg-image { max-width: 240px; border-radius: 10px; display: block; }
|
||||
|
||||
.system-msg {
|
||||
text-align: center; font-size: 12px; color: var(--color-text-hint);
|
||||
margin: 12px 0; padding: 4px 16px;
|
||||
background: rgba(0,0,0,0.03); border-radius: 12px; display: inline-block;
|
||||
}
|
||||
|
||||
.input-bar {
|
||||
display: flex; align-items: flex-end; gap: 8px;
|
||||
padding: 12px 20px; border-top: 1px solid var(--color-border);
|
||||
background: var(--color-surface);
|
||||
}
|
||||
.input-actions { display: flex; gap: 4px; padding-bottom: 4px; }
|
||||
.action-icon {
|
||||
font-size: 20px; cursor: pointer; opacity: 0.6; transition: opacity 0.2s;
|
||||
}
|
||||
.action-icon:hover { opacity: 1; }
|
||||
</style>
|
||||
|
||||
@@ -11,76 +11,43 @@
|
||||
<div class="search-bar">
|
||||
<n-input v-model:value="searchKeyword" placeholder="搜索会话..." size="small" clearable />
|
||||
</div>
|
||||
<!-- 布局模式切换 -->
|
||||
<div class="layout-switch">
|
||||
<n-button-group size="tiny">
|
||||
<n-button :type="uiStore.layoutMode === 'list' ? 'primary' : 'default'" @click="uiStore.setLayoutMode('list')">列表</n-button>
|
||||
<n-button :type="uiStore.layoutMode === 'card' ? 'primary' : 'default'" @click="uiStore.setLayoutMode('card')">卡片</n-button>
|
||||
<n-button :type="uiStore.layoutMode === 'waterfall' ? 'primary' : 'default'" @click="uiStore.setLayoutMode('waterfall')">瀑布流</n-button>
|
||||
</n-button-group>
|
||||
</div>
|
||||
<!-- 会话列表 -->
|
||||
<div class="conversation-list">
|
||||
<div v-if="chatStore.isLoading" style="text-align: center; padding: 40px; color: var(--color-text-hint)">加载中...</div>
|
||||
<div v-else-if="filteredConversations.length === 0" class="empty-state">
|
||||
<div style="font-size: 48px">💬</div>
|
||||
<p>{{ searchKeyword ? '没有找到匹配的会话' : '暂无消息' }}</p>
|
||||
<p style="font-size: 13px; color: var(--color-text-hint)">去通讯录找朋友聊天吧</p>
|
||||
<div v-if="chatStore.isLoading" class="loading-state">
|
||||
<div class="loading-spinner"></div>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
<!-- 列表模式 -->
|
||||
<template v-if="uiStore.layoutMode === 'list'">
|
||||
<div v-else-if="filteredConversations.length === 0" class="empty-state">
|
||||
<div class="empty-icon">💬</div>
|
||||
<p class="empty-title">{{ searchKeyword ? '没有找到匹配的会话' : '暂无消息' }}</p>
|
||||
<p class="empty-hint">去通讯录找朋友聊天吧</p>
|
||||
<n-button v-if="!searchKeyword" type="primary" size="small" @click="$router.push('/contacts/search')" style="margin-top: 12px">
|
||||
添加好友
|
||||
</n-button>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-for="conv in filteredConversations" :key="conv.id"
|
||||
class="conv-item" :class="{ active: chatStore.activeConversation === conv.id }"
|
||||
@click="openChat(conv.id)">
|
||||
<n-avatar :size="46" round :style="{ background: 'var(--color-primary)' }">
|
||||
{{ (conv.name || '?')[0] }}
|
||||
</n-avatar>
|
||||
<div class="conv-avatar-wrap">
|
||||
<n-avatar :size="48" round :style="avatarStyle(conv)">
|
||||
{{ (conv.name || '?')[0] }}
|
||||
</n-avatar>
|
||||
<span v-if="conv.type === 'group'" class="conv-type-badge">群</span>
|
||||
<span v-if="conv.unread_count > 0" class="unread-dot"></span>
|
||||
</div>
|
||||
<div class="conv-info">
|
||||
<div class="conv-top">
|
||||
<span class="conv-name">{{ conv.name || '未命名' }}</span>
|
||||
<span class="conv-time">{{ formatTime(conv.last_message_at) }}</span>
|
||||
</div>
|
||||
<div class="conv-bottom">
|
||||
<span class="conv-preview">{{ conv.last_message_preview || '' }}</span>
|
||||
<n-badge v-if="conv.unread_count > 0" :value="conv.unread_count" :max="99" />
|
||||
<span class="conv-preview">{{ conv.last_message_preview || '暂无消息' }}</span>
|
||||
<span v-if="conv.unread_count > 0" class="unread-badge">{{ conv.unread_count > 99 ? '99+' : conv.unread_count }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 卡片模式 -->
|
||||
<template v-else-if="uiStore.layoutMode === 'card'">
|
||||
<div class="card-grid">
|
||||
<div v-for="conv in filteredConversations" :key="conv.id"
|
||||
class="conv-card" :class="{ active: chatStore.activeConversation === conv.id }"
|
||||
@click="openChat(conv.id)">
|
||||
<n-avatar :size="56" round :style="{ background: 'var(--color-primary)' }">
|
||||
{{ (conv.name || '?')[0] }}
|
||||
</n-avatar>
|
||||
<div class="conv-card-name">{{ conv.name || '未命名' }}</div>
|
||||
<div class="conv-card-preview">{{ conv.last_message_preview?.substring(0, 30) || '' }}</div>
|
||||
<n-badge v-if="conv.unread_count > 0" :value="conv.unread_count" :max="99" style="position: absolute; top: 8px; right: 8px" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 瀑布流模式 -->
|
||||
<template v-else>
|
||||
<div class="waterfall-grid">
|
||||
<div v-for="conv in filteredConversations" :key="conv.id"
|
||||
class="conv-waterfall" @click="openChat(conv.id)">
|
||||
<div class="wf-header">
|
||||
<n-avatar :size="32" round :style="{ background: 'var(--color-primary)' }">
|
||||
{{ (conv.name || '?')[0] }}
|
||||
</n-avatar>
|
||||
<span class="wf-name">{{ conv.name }}</span>
|
||||
</div>
|
||||
<div class="wf-content">{{ conv.last_message_preview || '暂无消息' }}</div>
|
||||
<div class="wf-footer">
|
||||
<span>{{ formatTime(conv.last_message_at) }}</span>
|
||||
<n-badge v-if="conv.unread_count > 0" :value="conv.unread_count" :max="99" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 创建群聊弹窗 -->
|
||||
<CreateGroupModal :visible="showCreateGroup" @close="showCreateGroup = false" @created="onGroupCreated" />
|
||||
@@ -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')
|
||||
}
|
||||
</script>
|
||||
@@ -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); }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user