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']
|
||||
AppTabBar: typeof import('./src/components/common/AppTabBar.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']
|
||||
GroupCallBanner: typeof import('./src/components/call/GroupCallBanner.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"
|
||||
/>
|
||||
|
||||
<!-- 表情选择器 -->
|
||||
<emoji-picker
|
||||
:show="showEmoji"
|
||||
@select="handleEmojiSelect"
|
||||
@close="showEmoji = false"
|
||||
/>
|
||||
|
||||
<view class="input-bar-capsule">
|
||||
<!-- 左侧: 语音按钮 -->
|
||||
<view
|
||||
@@ -261,7 +268,7 @@
|
||||
</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="tool-item" @click="chooseImage">
|
||||
<view class="tool-icon album">
|
||||
@@ -358,6 +365,7 @@ import { useTheme } from '@/composables/useTheme'
|
||||
import { useWebRTC } from '@/composables/useWebRTC'
|
||||
import AppAvatar from '@/components/common/AppAvatar.vue'
|
||||
import MentionPicker from '@/components/chat/MentionPicker.vue'
|
||||
import EmojiPicker from '@/components/chat/EmojiPicker.vue'
|
||||
import type { MentionUser } from '@/components/chat/MentionPicker.vue'
|
||||
import * as roomApi from '@/api/modules/room'
|
||||
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 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 getMessageSenderAvatar(msg: ChatMessage): string { return targetUser.value?.avatar || '' }
|
||||
function getMessageSenderName(msg: ChatMessage): string { return targetUser.value?.name || chatName.value || '未知' }
|
||||
function getMessageSenderAvatar(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?.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 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 } }
|
||||
@@ -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 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 handleEmojiSelect(emoji: string) { inputText.value += emoji }
|
||||
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; }
|
||||
}
|
||||
|
||||
// 消息项容器
|
||||
// 消息项容器 - 与设计稿一致
|
||||
.message-item {
|
||||
margin-bottom: 40rpx;
|
||||
margin-bottom: 48rpx;
|
||||
|
||||
.timestamp {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 30rpx;
|
||||
margin-bottom: 32rpx;
|
||||
|
||||
text {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
padding: 6rpx 18rpx;
|
||||
padding: 8rpx 20rpx;
|
||||
border-radius: 20rpx;
|
||||
font-size: 22rpx;
|
||||
font-size: 24rpx;
|
||||
color: var(--text-tertiary);
|
||||
|
||||
.theme-dark & { background: rgba(255, 255, 255, 0.08); }
|
||||
@@ -646,44 +671,44 @@ async function sendVoiceMessage(filePath: string, duration: number) { toast.load
|
||||
|
||||
.system-notice {
|
||||
text-align: center;
|
||||
margin: 20rpx 0;
|
||||
margin: 24rpx 0;
|
||||
text { font-size: 24rpx; color: var(--text-tertiary); }
|
||||
}
|
||||
}
|
||||
|
||||
// 核心布局
|
||||
// 核心布局 - 与设计稿一致
|
||||
.bubble-group {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16rpx;
|
||||
gap: 24rpx;
|
||||
|
||||
&.is-me {
|
||||
justify-content: flex-end;
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
}
|
||||
|
||||
// 头像容器
|
||||
// 头像容器 - 与设计稿一致(40px=80rpx)
|
||||
.avatar-box {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.left { order: 1; }
|
||||
&.right { order: 3; }
|
||||
:deep(.app-avatar) {
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// 内容容器
|
||||
// 内容容器 - 与设计稿一致
|
||||
.content-box {
|
||||
order: 2;
|
||||
max-width: 75%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.sender-label {
|
||||
font-size: 22rpx;
|
||||
font-size: 20rpx;
|
||||
color: var(--text-tertiary);
|
||||
margin-bottom: 8rpx;
|
||||
margin-left: 12rpx;
|
||||
margin-left: 4rpx;
|
||||
}
|
||||
|
||||
.is-me & {
|
||||
@@ -698,16 +723,17 @@ async function sendVoiceMessage(filePath: string, duration: number) { toast.load
|
||||
padding: 24rpx 28rpx;
|
||||
border-radius: 32rpx;
|
||||
font-size: 30rpx;
|
||||
line-height: 1.6;
|
||||
line-height: 1.5;
|
||||
position: relative;
|
||||
word-break: break-all;
|
||||
max-width: 100%;
|
||||
|
||||
// 对方气泡 - 白色背景
|
||||
&.bubble-other {
|
||||
background: var(--bubble-other-bg);
|
||||
color: var(--text-msg-other);
|
||||
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 & {
|
||||
border: 1rpx solid var(--bubble-other-border);
|
||||
@@ -732,7 +758,7 @@ async function sendVoiceMessage(filePath: string, duration: number) { toast.load
|
||||
padding: 24rpx 28rpx;
|
||||
background: var(--bubble-other-bg) !important;
|
||||
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 & {
|
||||
border: 1rpx solid var(--bubble-other-border) !important;
|
||||
@@ -992,7 +1018,7 @@ async function sendVoiceMessage(filePath: string, duration: number) { toast.load
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 99;
|
||||
z-index: 200;
|
||||
background: var(--bg-card);
|
||||
border-top: 1rpx solid var(--border-subtle);
|
||||
padding: 24rpx 32rpx;
|
||||
@@ -1039,7 +1065,7 @@ async function sendVoiceMessage(filePath: string, duration: number) { toast.load
|
||||
flex: 1;
|
||||
min-height: 88rpx;
|
||||
background: var(--input-bg);
|
||||
border-radius: 40rpx;
|
||||
border-radius: 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx 28rpx;
|
||||
@@ -1052,9 +1078,9 @@ async function sendVoiceMessage(filePath: string, duration: number) { toast.load
|
||||
|
||||
.text-input {
|
||||
width: 100%;
|
||||
min-height: 48rpx;
|
||||
min-height: 40rpx;
|
||||
max-height: 200rpx;
|
||||
font-size: 30rpx;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.5;
|
||||
color: var(--text-main);
|
||||
background: transparent;
|
||||
|
||||
@@ -87,15 +87,12 @@
|
||||
>
|
||||
<!-- 头像 -->
|
||||
<view class="avatar-wrap">
|
||||
<image
|
||||
v-if="item.avatar"
|
||||
class="conv-avatar"
|
||||
:src="resolveImageUrl(item.avatar)"
|
||||
mode="aspectFill"
|
||||
<app-avatar
|
||||
:src="item.avatar"
|
||||
:name="item.name"
|
||||
:size="100"
|
||||
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>
|
||||
<!-- 未读红点/数字 -->
|
||||
@@ -185,6 +182,7 @@ import * as conversationApi from '@/api/modules/conversation'
|
||||
import AppTabBar from '@/components/common/AppTabBar.vue'
|
||||
import AppDrawer from '@/components/common/AppDrawer.vue'
|
||||
import PlusMenu from '@/components/common/PlusMenu.vue'
|
||||
import AppAvatar from '@/components/common/AppAvatar.vue'
|
||||
import type { Conversation } from '@/types/conversation'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
Reference in New Issue
Block a user