UI设计
This commit is contained in:
1
components.d.ts
vendored
1
components.d.ts
vendored
@@ -16,6 +16,7 @@ declare module 'vue' {
|
|||||||
AppNavBar: typeof import('./src/components/common/AppNavBar.vue')['default']
|
AppNavBar: typeof import('./src/components/common/AppNavBar.vue')['default']
|
||||||
AppTabBar: typeof import('./src/components/common/AppTabBar.vue')['default']
|
AppTabBar: typeof import('./src/components/common/AppTabBar.vue')['default']
|
||||||
CallWindow: typeof import('./src/components/call/CallWindow.vue')['default']
|
CallWindow: typeof import('./src/components/call/CallWindow.vue')['default']
|
||||||
|
EmojiPicker: typeof import('./src/components/chat/EmojiPicker.vue')['default']
|
||||||
GlobalCallProvider: typeof import('./src/components/call/GlobalCallProvider.vue')['default']
|
GlobalCallProvider: typeof import('./src/components/call/GlobalCallProvider.vue')['default']
|
||||||
GroupCallBanner: typeof import('./src/components/call/GroupCallBanner.vue')['default']
|
GroupCallBanner: typeof import('./src/components/call/GroupCallBanner.vue')['default']
|
||||||
GroupCallIncoming: typeof import('./src/components/call/GroupCallIncoming.vue')['default']
|
GroupCallIncoming: typeof import('./src/components/call/GroupCallIncoming.vue')['default']
|
||||||
|
|||||||
247
src/components/chat/EmojiPicker.vue
Normal file
247
src/components/chat/EmojiPicker.vue
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
<template>
|
||||||
|
<view v-if="show" class="emoji-picker" :class="{ dark: isDark }">
|
||||||
|
<!-- 分类标签 -->
|
||||||
|
<view class="emoji-tabs">
|
||||||
|
<view
|
||||||
|
v-for="(cat, idx) in categories"
|
||||||
|
:key="cat.key"
|
||||||
|
class="tab-item"
|
||||||
|
:class="{ active: activeTab === idx }"
|
||||||
|
@click="activeTab = idx"
|
||||||
|
>
|
||||||
|
<text>{{ cat.icon }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 表情列表 -->
|
||||||
|
<scroll-view scroll-y class="emoji-grid-container">
|
||||||
|
<view class="emoji-grid">
|
||||||
|
<view
|
||||||
|
v-for="emoji in currentEmojis"
|
||||||
|
:key="emoji"
|
||||||
|
class="emoji-item"
|
||||||
|
@click="selectEmoji(emoji)"
|
||||||
|
>
|
||||||
|
{{ emoji }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { useTheme } from '@/composables/useTheme'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
show: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'select', emoji: string): void
|
||||||
|
(e: 'close'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { isDark } = useTheme()
|
||||||
|
const activeTab = ref(0)
|
||||||
|
|
||||||
|
// 表情分类
|
||||||
|
const categories = [
|
||||||
|
{
|
||||||
|
key: 'face',
|
||||||
|
icon: '😀',
|
||||||
|
emojis: [
|
||||||
|
'😀', '😃', '😄', '😁', '😆', '😅', '🤣', '😂',
|
||||||
|
'🙂', '🙃', '😉', '😊', '😇', '🥰', '😍', '🤩',
|
||||||
|
'😘', '😗', '☺️', '😚', '😙', '🥲', '😋', '😛',
|
||||||
|
'😜', '🤪', '😝', '🤑', '🤗', '🤭', '🤫', '🤔',
|
||||||
|
'🤐', '🤨', '😐', '😑', '😶', '😏', '😒', '🙄',
|
||||||
|
'😬', '🤥', '😌', '😔', '😪', '🤤', '😴', '😷',
|
||||||
|
'🤒', '🤕', '🤢', '🤮', '🤧', '🥵', '🥶', '🥴',
|
||||||
|
'😵', '🤯', '🤠', '🥳', '🥸', '😎', '🤓', '🧐'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'gesture',
|
||||||
|
icon: '👋',
|
||||||
|
emojis: [
|
||||||
|
'👋', '🤚', '🖐️', '✋', '🖖', '👌', '🤌', '🤏',
|
||||||
|
'✌️', '🤞', '🤟', '🤘', '🤙', '👈', '👉', '👆',
|
||||||
|
'🖕', '👇', '☝️', '👍', '👎', '✊', '👊', '🤛',
|
||||||
|
'🤜', '👏', '🙌', '👐', '🤲', '🤝', '🙏', '✍️',
|
||||||
|
'💪', '🦾', '🦿', '🦵', '🦶', '👂', '🦻', '👃'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'heart',
|
||||||
|
icon: '❤️',
|
||||||
|
emojis: [
|
||||||
|
'❤️', '🧡', '💛', '💚', '💙', '💜', '🖤', '🤍',
|
||||||
|
'🤎', '💔', '❣️', '💕', '💞', '💓', '💗', '💖',
|
||||||
|
'💘', '💝', '💟', '☮️', '✝️', '☪️', '🕉️', '☸️',
|
||||||
|
'✡️', '🔯', '🕎', '☯️', '☦️', '🛐', '⛎', '♈'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'animal',
|
||||||
|
icon: '🐶',
|
||||||
|
emojis: [
|
||||||
|
'🐶', '🐱', '🐭', '🐹', '🐰', '🦊', '🐻', '🐼',
|
||||||
|
'🐻❄️', '🐨', '🐯', '🦁', '🐮', '🐷', '🐸', '🐵',
|
||||||
|
'🙈', '🙉', '🙊', '🐒', '🐔', '🐧', '🐦', '🐤',
|
||||||
|
'🐣', '🐥', '🦆', '🦅', '🦉', '🦇', '🐺', '🐗'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'food',
|
||||||
|
icon: '🍔',
|
||||||
|
emojis: [
|
||||||
|
'🍎', '🍐', '🍊', '🍋', '🍌', '🍉', '🍇', '🍓',
|
||||||
|
'🫐', '🍈', '🍒', '🍑', '🥭', '🍍', '🥥', '🥝',
|
||||||
|
'🍔', '🍟', '🍕', '🌭', '🥪', '🌮', '🌯', '🥗',
|
||||||
|
'🍜', '🍝', '🍲', '🍱', '🍣', '🍤', '🍿', '🧁'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'activity',
|
||||||
|
icon: '⚽',
|
||||||
|
emojis: [
|
||||||
|
'⚽', '🏀', '🏈', '⚾', '🥎', '🎾', '🏐', '🏉',
|
||||||
|
'🥏', '🎱', '🪀', '🏓', '🏸', '🏒', '🏑', '🥍',
|
||||||
|
'🏏', '🪃', '🥅', '⛳', '🪁', '🏹', '🎣', '🤿',
|
||||||
|
'🥊', '🥋', '🎽', '🛹', '🛼', '🛷', '⛸️', '🥌'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'object',
|
||||||
|
icon: '💡',
|
||||||
|
emojis: [
|
||||||
|
'⌚', '📱', '💻', '⌨️', '🖥️', '🖨️', '🖱️', '🖲️',
|
||||||
|
'💽', '💾', '💿', '📀', '📼', '📷', '📸', '📹',
|
||||||
|
'🎥', '📽️', '🎞️', '📞', '☎️', '📟', '📠', '📺',
|
||||||
|
'📻', '🎙️', '🎚️', '🎛️', '🧭', '⏱️', '⏲️', '⏰'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'symbol',
|
||||||
|
icon: '🔥',
|
||||||
|
emojis: [
|
||||||
|
'🔥', '💥', '✨', '🌟', '⭐', '🌈', '☀️', '🌤️',
|
||||||
|
'⛅', '🌥️', '☁️', '🌦️', '🌧️', '⛈️', '🌩️', '🌨️',
|
||||||
|
'❄️', '☃️', '⛄', '🌬️', '💨', '💧', '💦', '☔',
|
||||||
|
'🎵', '🎶', '🎼', '🎹', '🥁', '🎷', '🎺', '🎸'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const currentEmojis = computed(() => categories[activeTab.value]?.emojis || [])
|
||||||
|
|
||||||
|
function selectEmoji(emoji: string) {
|
||||||
|
emit('select', emoji)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.emoji-picker {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: var(--bg-card, #fff);
|
||||||
|
border-radius: 24rpx 24rpx 0 0;
|
||||||
|
box-shadow: 0 -8rpx 32rpx rgba(0, 0, 0, 0.1);
|
||||||
|
z-index: 100;
|
||||||
|
animation: slideUp 0.25s ease-out;
|
||||||
|
|
||||||
|
&.dark {
|
||||||
|
background: #292524;
|
||||||
|
box-shadow: 0 -8rpx 32rpx rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
transform: translateY(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji-tabs {
|
||||||
|
display: flex;
|
||||||
|
padding: 16rpx 24rpx;
|
||||||
|
gap: 8rpx;
|
||||||
|
border-bottom: 1rpx solid rgba(0, 0, 0, 0.05);
|
||||||
|
overflow-x: auto;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
border-bottom-color: #44403c;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item {
|
||||||
|
min-width: 72rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
font-size: 40rpx;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: rgba(79, 70, 229, 0.1);
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
background: rgba(249, 115, 22, 0.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji-grid-container {
|
||||||
|
height: 400rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(8, 1fr);
|
||||||
|
gap: 8rpx;
|
||||||
|
padding: 16rpx 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji-item {
|
||||||
|
aspect-ratio: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 48rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.9);
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark &:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -197,6 +197,13 @@
|
|||||||
@close="showMentionPicker = false"
|
@close="showMentionPicker = false"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- 表情选择器 -->
|
||||||
|
<emoji-picker
|
||||||
|
:show="showEmoji"
|
||||||
|
@select="handleEmojiSelect"
|
||||||
|
@close="showEmoji = false"
|
||||||
|
/>
|
||||||
|
|
||||||
<view class="input-bar-capsule">
|
<view class="input-bar-capsule">
|
||||||
<!-- 左侧: 语音按钮 -->
|
<!-- 左侧: 语音按钮 -->
|
||||||
<view
|
<view
|
||||||
@@ -261,7 +268,7 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 更多功能面板 -->
|
<!-- 更多功能面板 -->
|
||||||
<wd-popup v-model="showMore" position="bottom" safe-area-inset-bottom custom-style="background: var(--bg-card); border-radius: 32rpx 32rpx 0 0;">
|
<wd-popup v-model="showMore" position="bottom" safe-area-inset-bottom :z-index="10000" custom-style="background: var(--bg-card); border-radius: 32rpx 32rpx 0 0;">
|
||||||
<view class="tools-grid">
|
<view class="tools-grid">
|
||||||
<view class="tool-item" @click="chooseImage">
|
<view class="tool-item" @click="chooseImage">
|
||||||
<view class="tool-icon album">
|
<view class="tool-icon album">
|
||||||
@@ -358,6 +365,7 @@ import { useTheme } from '@/composables/useTheme'
|
|||||||
import { useWebRTC } from '@/composables/useWebRTC'
|
import { useWebRTC } from '@/composables/useWebRTC'
|
||||||
import AppAvatar from '@/components/common/AppAvatar.vue'
|
import AppAvatar from '@/components/common/AppAvatar.vue'
|
||||||
import MentionPicker from '@/components/chat/MentionPicker.vue'
|
import MentionPicker from '@/components/chat/MentionPicker.vue'
|
||||||
|
import EmojiPicker from '@/components/chat/EmojiPicker.vue'
|
||||||
import type { MentionUser } from '@/components/chat/MentionPicker.vue'
|
import type { MentionUser } from '@/components/chat/MentionPicker.vue'
|
||||||
import * as roomApi from '@/api/modules/room'
|
import * as roomApi from '@/api/modules/room'
|
||||||
import type { ChatMessage, Attachment, GroupMember } from '@/types/api'
|
import type { ChatMessage, Attachment, GroupMember } from '@/types/api'
|
||||||
@@ -420,8 +428,24 @@ function getMediaTypeClass(type: number) { if (type === 1) return 'media-bubble
|
|||||||
function getMediaUrl(msg: ChatMessage): string { let url = ''; if (typeof msg.extra === 'string') { try { const extra = JSON.parse(msg.extra); url = extra.url || msg.content } catch { url = msg.content } } else { url = msg.extra?.url || msg.content } return resolveImageUrl(url) }
|
function getMediaUrl(msg: ChatMessage): string { let url = ''; if (typeof msg.extra === 'string') { try { const extra = JSON.parse(msg.extra); url = extra.url || msg.content } catch { url = msg.content } } else { url = msg.extra?.url || msg.content } return resolveImageUrl(url) }
|
||||||
function getFileName(msg: ChatMessage): string { if (typeof msg.extra === 'string') { try { const extra = JSON.parse(msg.extra); return extra.name || '未知文件' } catch { return '未知文件' } } return msg.extra?.name || '未知文件' }
|
function getFileName(msg: ChatMessage): string { if (typeof msg.extra === 'string') { try { const extra = JSON.parse(msg.extra); return extra.name || '未知文件' } catch { return '未知文件' } } return msg.extra?.name || '未知文件' }
|
||||||
function getFileSize(msg: ChatMessage): number { if (typeof msg.extra === 'string') { try { const extra = JSON.parse(msg.extra); return extra.size || 0 } catch { return 0 } } return msg.extra?.size || 0 }
|
function getFileSize(msg: ChatMessage): number { if (typeof msg.extra === 'string') { try { const extra = JSON.parse(msg.extra); return extra.size || 0 } catch { return 0 } } return msg.extra?.size || 0 }
|
||||||
function getMessageSenderAvatar(msg: ChatMessage): string { return targetUser.value?.avatar || '' }
|
function getMessageSenderAvatar(msg: ChatMessage): string {
|
||||||
function getMessageSenderName(msg: ChatMessage): string { return targetUser.value?.name || chatName.value || '未知' }
|
// 群聊时从群成员列表获取头像
|
||||||
|
if (isGroupChat.value && msg.sender_user_id) {
|
||||||
|
const member = groupMembers.value.find(m => m.user_id === msg.sender_user_id)
|
||||||
|
if (member?.user?.avatar) return member.user.avatar
|
||||||
|
}
|
||||||
|
// 单聊时使用对方头像
|
||||||
|
return targetUser.value?.avatar || ''
|
||||||
|
}
|
||||||
|
function getMessageSenderName(msg: ChatMessage): string {
|
||||||
|
// 群聊时从群成员列表获取名称
|
||||||
|
if (isGroupChat.value && msg.sender_user_id) {
|
||||||
|
const member = groupMembers.value.find(m => m.user_id === msg.sender_user_id)
|
||||||
|
if (member) return member.nickname || member.user?.name || '群成员'
|
||||||
|
}
|
||||||
|
// 单聊时使用对方名称
|
||||||
|
return targetUser.value?.name || chatName.value || '未知'
|
||||||
|
}
|
||||||
function onAvatarClick(msg: ChatMessage) { const userId = msg.sender_user_id; if (!userId) return; uni.navigateTo({ url: `/pages/contact/detail?userId=${userId}` }) }
|
function onAvatarClick(msg: ChatMessage) { const userId = msg.sender_user_id; if (!userId) return; uni.navigateTo({ url: `/pages/contact/detail?userId=${userId}` }) }
|
||||||
function handleMessageLongPress(msg: ChatMessage) { selectedMessage.value = msg; showMsgActions.value = true }
|
function handleMessageLongPress(msg: ChatMessage) { selectedMessage.value = msg; showMsgActions.value = true }
|
||||||
async function onMsgActionSelect(action: { value: string }) { if (!selectedMessage.value) return; const msg = selectedMessage.value; showMsgActions.value = false; switch (action.value) { case 'copy': copyMessage(msg); break; case 'recall': await recallMessage(msg); break; case 'delete': deleteMessage(msg); break } }
|
async function onMsgActionSelect(action: { value: string }) { if (!selectedMessage.value) return; const msg = selectedMessage.value; showMsgActions.value = false; switch (action.value) { case 'copy': copyMessage(msg); break; case 'recall': await recallMessage(msg); break; case 'delete': deleteMessage(msg); break } }
|
||||||
@@ -431,6 +455,7 @@ function deleteMessage(msg: ChatMessage) { const index = messages.value.findInde
|
|||||||
function onInputChange(e: any) { const value = e.detail?.value || inputText.value; if (value.endsWith('@') && isGroupChat.value) { showMentionPicker.value = true } }
|
function onInputChange(e: any) { const value = e.detail?.value || inputText.value; if (value.endsWith('@') && isGroupChat.value) { showMentionPicker.value = true } }
|
||||||
function triggerMention() { inputText.value += '@'; showMentionPicker.value = true }
|
function triggerMention() { inputText.value += '@'; showMentionPicker.value = true }
|
||||||
function handleMentionSelect(user: MentionUser) { if (inputText.value.endsWith('@')) { inputText.value = inputText.value.slice(0, -1) + `@${user.name} ` } else { inputText.value += `@${user.name} ` } showMentionPicker.value = false }
|
function handleMentionSelect(user: MentionUser) { if (inputText.value.endsWith('@')) { inputText.value = inputText.value.slice(0, -1) + `@${user.name} ` } else { inputText.value += `@${user.name} ` } showMentionPicker.value = false }
|
||||||
|
function handleEmojiSelect(emoji: string) { inputText.value += emoji }
|
||||||
function previewImage(msg: ChatMessage) { const url = getMediaUrl(msg); if (url) { uni.previewImage({ urls: [url], current: url }) } }
|
function previewImage(msg: ChatMessage) { const url = getMediaUrl(msg); if (url) { uni.previewImage({ urls: [url], current: url }) } }
|
||||||
|
|
||||||
// ========== 语音录制处理 ==========
|
// ========== 语音录制处理 ==========
|
||||||
@@ -624,20 +649,20 @@ async function sendVoiceMessage(filePath: string, duration: number) { toast.load
|
|||||||
.end-text { color: var(--text-tertiary); font-size: 24rpx; }
|
.end-text { color: var(--text-tertiary); font-size: 24rpx; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 消息项容器
|
// 消息项容器 - 与设计稿一致
|
||||||
.message-item {
|
.message-item {
|
||||||
margin-bottom: 40rpx;
|
margin-bottom: 48rpx;
|
||||||
|
|
||||||
.timestamp {
|
.timestamp {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-bottom: 30rpx;
|
margin-bottom: 32rpx;
|
||||||
|
|
||||||
text {
|
text {
|
||||||
background: rgba(0, 0, 0, 0.04);
|
background: rgba(0, 0, 0, 0.04);
|
||||||
padding: 6rpx 18rpx;
|
padding: 8rpx 20rpx;
|
||||||
border-radius: 20rpx;
|
border-radius: 20rpx;
|
||||||
font-size: 22rpx;
|
font-size: 24rpx;
|
||||||
color: var(--text-tertiary);
|
color: var(--text-tertiary);
|
||||||
|
|
||||||
.theme-dark & { background: rgba(255, 255, 255, 0.08); }
|
.theme-dark & { background: rgba(255, 255, 255, 0.08); }
|
||||||
@@ -646,44 +671,44 @@ async function sendVoiceMessage(filePath: string, duration: number) { toast.load
|
|||||||
|
|
||||||
.system-notice {
|
.system-notice {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 20rpx 0;
|
margin: 24rpx 0;
|
||||||
text { font-size: 24rpx; color: var(--text-tertiary); }
|
text { font-size: 24rpx; color: var(--text-tertiary); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 核心布局
|
// 核心布局 - 与设计稿一致
|
||||||
.bubble-group {
|
.bubble-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 16rpx;
|
gap: 24rpx;
|
||||||
|
|
||||||
&.is-me {
|
&.is-me {
|
||||||
justify-content: flex-end;
|
flex-direction: row-reverse;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 头像容器
|
// 头像容器 - 与设计稿一致(40px=80rpx)
|
||||||
.avatar-box {
|
.avatar-box {
|
||||||
width: 80rpx;
|
width: 80rpx;
|
||||||
height: 80rpx;
|
height: 80rpx;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
&.left { order: 1; }
|
:deep(.app-avatar) {
|
||||||
&.right { order: 3; }
|
border-radius: 24rpx;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 内容容器
|
// 内容容器 - 与设计稿一致
|
||||||
.content-box {
|
.content-box {
|
||||||
order: 2;
|
|
||||||
max-width: 75%;
|
max-width: 75%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
.sender-label {
|
.sender-label {
|
||||||
font-size: 22rpx;
|
font-size: 20rpx;
|
||||||
color: var(--text-tertiary);
|
color: var(--text-tertiary);
|
||||||
margin-bottom: 8rpx;
|
margin-bottom: 8rpx;
|
||||||
margin-left: 12rpx;
|
margin-left: 4rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-me & {
|
.is-me & {
|
||||||
@@ -698,16 +723,17 @@ async function sendVoiceMessage(filePath: string, duration: number) { toast.load
|
|||||||
padding: 24rpx 28rpx;
|
padding: 24rpx 28rpx;
|
||||||
border-radius: 32rpx;
|
border-radius: 32rpx;
|
||||||
font-size: 30rpx;
|
font-size: 30rpx;
|
||||||
line-height: 1.6;
|
line-height: 1.5;
|
||||||
position: relative;
|
position: relative;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
// 对方气泡 - 白色背景
|
// 对方气泡 - 白色背景
|
||||||
&.bubble-other {
|
&.bubble-other {
|
||||||
background: var(--bubble-other-bg);
|
background: var(--bubble-other-bg);
|
||||||
color: var(--text-msg-other);
|
color: var(--text-msg-other);
|
||||||
border-bottom-left-radius: 8rpx;
|
border-bottom-left-radius: 8rpx;
|
||||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.05);
|
||||||
|
|
||||||
.theme-dark & {
|
.theme-dark & {
|
||||||
border: 1rpx solid var(--bubble-other-border);
|
border: 1rpx solid var(--bubble-other-border);
|
||||||
@@ -732,7 +758,7 @@ async function sendVoiceMessage(filePath: string, duration: number) { toast.load
|
|||||||
padding: 24rpx 28rpx;
|
padding: 24rpx 28rpx;
|
||||||
background: var(--bubble-other-bg) !important;
|
background: var(--bubble-other-bg) !important;
|
||||||
border-radius: 32rpx;
|
border-radius: 32rpx;
|
||||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05) !important;
|
box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.05) !important;
|
||||||
|
|
||||||
.theme-dark & {
|
.theme-dark & {
|
||||||
border: 1rpx solid var(--bubble-other-border) !important;
|
border: 1rpx solid var(--bubble-other-border) !important;
|
||||||
@@ -992,7 +1018,7 @@ async function sendVoiceMessage(filePath: string, duration: number) { toast.load
|
|||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
z-index: 99;
|
z-index: 200;
|
||||||
background: var(--bg-card);
|
background: var(--bg-card);
|
||||||
border-top: 1rpx solid var(--border-subtle);
|
border-top: 1rpx solid var(--border-subtle);
|
||||||
padding: 24rpx 32rpx;
|
padding: 24rpx 32rpx;
|
||||||
@@ -1039,7 +1065,7 @@ async function sendVoiceMessage(filePath: string, duration: number) { toast.load
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 88rpx;
|
min-height: 88rpx;
|
||||||
background: var(--input-bg);
|
background: var(--input-bg);
|
||||||
border-radius: 40rpx;
|
border-radius: 32rpx;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 20rpx 28rpx;
|
padding: 20rpx 28rpx;
|
||||||
@@ -1052,9 +1078,9 @@ async function sendVoiceMessage(filePath: string, duration: number) { toast.load
|
|||||||
|
|
||||||
.text-input {
|
.text-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 48rpx;
|
min-height: 40rpx;
|
||||||
max-height: 200rpx;
|
max-height: 200rpx;
|
||||||
font-size: 30rpx;
|
font-size: 28rpx;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
color: var(--text-main);
|
color: var(--text-main);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
|||||||
@@ -87,15 +87,12 @@
|
|||||||
>
|
>
|
||||||
<!-- 头像 -->
|
<!-- 头像 -->
|
||||||
<view class="avatar-wrap">
|
<view class="avatar-wrap">
|
||||||
<image
|
<app-avatar
|
||||||
v-if="item.avatar"
|
:src="item.avatar"
|
||||||
class="conv-avatar"
|
:name="item.name"
|
||||||
:src="resolveImageUrl(item.avatar)"
|
:size="100"
|
||||||
mode="aspectFill"
|
radius="20rpx"
|
||||||
/>
|
/>
|
||||||
<view v-else class="conv-avatar avatar-placeholder">
|
|
||||||
{{ (item.name || '?').charAt(0) }}
|
|
||||||
</view>
|
|
||||||
<!-- 在线状态点 (单聊时显示) -->
|
<!-- 在线状态点 (单聊时显示) -->
|
||||||
<view v-if="item.is_online && !item.is_group" class="online-indicator"></view>
|
<view v-if="item.is_online && !item.is_group" class="online-indicator"></view>
|
||||||
<!-- 未读红点/数字 -->
|
<!-- 未读红点/数字 -->
|
||||||
@@ -185,6 +182,7 @@ import * as conversationApi from '@/api/modules/conversation'
|
|||||||
import AppTabBar from '@/components/common/AppTabBar.vue'
|
import AppTabBar from '@/components/common/AppTabBar.vue'
|
||||||
import AppDrawer from '@/components/common/AppDrawer.vue'
|
import AppDrawer from '@/components/common/AppDrawer.vue'
|
||||||
import PlusMenu from '@/components/common/PlusMenu.vue'
|
import PlusMenu from '@/components/common/PlusMenu.vue'
|
||||||
|
import AppAvatar from '@/components/common/AppAvatar.vue'
|
||||||
import type { Conversation } from '@/types/conversation'
|
import type { Conversation } from '@/types/conversation'
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
|||||||
Reference in New Issue
Block a user