This commit is contained in:
2025-12-11 22:42:10 +08:00
parent 408fe9294a
commit ff0ba0d311
7 changed files with 222 additions and 173 deletions

View File

@@ -307,39 +307,45 @@ defineExpose({
</script>
<style lang="scss" scoped>
// ==========================================
// 与设计稿 聊天页面.html 第116-123行 完全一致
// ==========================================
.message-input {
--input-bg: #f1f1f1;
--input-text: #1f2937;
--placeholder-color: #9ca3af;
--icon-color: #6b7280;
--icon-hover: #4F46E5;
// 亮色模式颜色 (与设计稿一致)
--input-bg: #f3f4f6; // 设计稿: bg-gray-100
--input-text: #1f2937; // 设计稿: text-gray-800
--placeholder-color: #9ca3af; // 设计稿: placeholder-gray-400
--icon-color: #6b7280; // 设计稿: text-gray-500
--icon-hover: #4F46E5; // 设计稿: hover:text-indigo-600
--send-bg: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%);
--border-color: rgba(0, 0, 0, 0.05);
--bg-content: #ffffff;
--border-color: #f3f4f6; // 设计稿: border-gray-100
--bg-content: #ffffff; // 设计稿: bg-white
position: relative;
background: var(--bg-content);
border-top: 1rpx solid var(--border-color);
border-top: 2rpx solid var(--border-color);
transition: all 0.3s;
// 暗色模式 - Warm Stone (与设计稿一致)
&.theme-dark {
--input-bg: #292524;
--input-text: #f5f5f4;
--placeholder-color: #78716c;
--icon-color: #a8a29e;
--icon-hover: #f97316;
--input-bg: #292524; // 设计稿: dark:bg-warm-800
--input-text: #f5f5f4; // 设计稿: dark:text-warm-100
--placeholder-color: #78716c; // 设计稿: dark:placeholder-warm-500
--icon-color: #a8a29e; // 设计稿: dark:text-warm-400
--icon-hover: #f97316; // 设计稿: dark:hover:text-orange-400
--send-bg: linear-gradient(135deg, #fb923c 0%, #f97316 100%);
--border-color: #44403c;
--bg-content: #1c1917;
--border-color: #292524; // 设计稿: dark:border-warm-800
--bg-content: #1c1917; // 设计稿: dark:bg-warm-900
}
}
// 底部输入栏
// 底部输入栏 - 设计稿: px-4 py-3 pb-8 flex items-end gap-3
.input-bar {
display: flex;
align-items: flex-end;
gap: 24rpx;
padding: 24rpx 32rpx;
padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
align-items: flex-end; // 设计稿: items-end
gap: 24rpx; // 设计稿: gap-3 = 12px = 24rpx
padding: 24rpx 32rpx; // 设计稿: px-4 py-3 = 16px 12px = 32rpx 24rpx
padding-bottom: calc(64rpx + env(safe-area-inset-bottom)); // 设计稿: pb-8 = 32px = 64rpx
}
// 语音按钮
@@ -365,27 +371,32 @@ defineExpose({
}
}
// 输入框容器
// 输入框容器 - 与设计稿完全一致
// 设计稿: flex-1 bg-gray-100 dark:bg-warm-800 rounded-2xl px-4 py-2.5 min-h-[44px]
// border border-transparent dark:border-warm-700
.input-wrap {
flex: 1;
min-height: 88rpx;
min-height: 88rpx; // 设计稿: min-h-[44px] = 44px = 88rpx
background: var(--input-bg);
border-radius: 40rpx;
padding: 20rpx 28rpx;
border-radius: 32rpx; // 设计稿: rounded-2xl = 16px = 32rpx
padding: 20rpx 32rpx; // 设计稿: px-4 py-2.5 = 16px 10px = 32rpx 20rpx
display: flex;
align-items: center;
border: 2rpx solid transparent; // 设计稿: border border-transparent
transition: all 0.2s;
// 设计稿: dark:border-warm-700
.theme-dark & {
border: 1rpx solid var(--border-color);
border-color: #44403c; // warm-700
}
}
// 设计稿: text-sm text-gray-800 dark:text-warm-100 resize-none h-5
.text-input {
width: 100%;
min-height: 48rpx;
min-height: 40rpx; // 设计稿: h-5 = 20px = 40rpx
max-height: 200rpx;
font-size: 30rpx;
font-size: 28rpx; // 设计稿: text-sm = 14px = 28rpx
line-height: 1.5;
color: var(--input-text);
background: transparent;

View File

@@ -83,7 +83,9 @@ function handleClickRight() {
:deep(.app-nav-bar) {
// 适配暗黑模式的背景色和文字颜色
// 假设 src/uni.scss 或 variables.scss 中定义了这些 CSS 变量
background-color: var(--bg-content) !important;
background-color: rgba(255, 255, 255, 0.85) !important;
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
.wd-navbar__title {
color: var(--text-primary) !important;
@@ -102,10 +104,16 @@ function handleClickRight() {
// 暗黑模式下的特殊覆盖(如果 CSS 变量不够用)
.dark {
:deep(.app-nav-bar) {
background-color: #1c1c1e !important; // 兜底暗色
background-color: rgba(28, 25, 23, 0.85) !important; // Warm Stone 900 with transparency
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
.wd-navbar__title {
color: #ffffff !important;
color: #f5f5f4 !important;
}
.wd-icon {
color: #e7e5e4 !important;
}
}
}

View File

@@ -193,6 +193,20 @@ function onAction() {
background: var(--bg-hover);
border-radius: 8rpx;
padding: 16rpx;
position: relative;
// 三角指示器
&::before {
content: '';
position: absolute;
top: -12rpx;
left: 24rpx;
width: 0;
height: 0;
border-left: 12rpx solid transparent;
border-right: 12rpx solid transparent;
border-bottom: 12rpx solid var(--bg-hover);
}
.likes-row {
display: flex;

View File

@@ -127,9 +127,9 @@
<!-- 视频 -->
<view v-else-if="msg.message_type === 3" class="video-preview" @click="playVideo(msg)">
<image
:src="getVideoThumb(msg)"
mode="aspectFill"
<image
:src="getVideoThumb(msg)"
mode="aspectFill"
class="video-thumb"
/>
<view class="play-overlay">
@@ -401,12 +401,12 @@ onLoad((options: any) => { roomId.value = options?.roomId || ''; targetId.value
onMounted(() => { if (targetId.value) { conversationStore.clearUnread(targetId.value) } setTimeout(() => { scrollToBottom(false) }, 300) });
onUnmounted(() => { wsManager.offMessage(handleNewMessage); stopAudio(); });
async function loadGroupMembers() {
async function loadGroupMembers() {
if (!roomId.value) return
try {
try {
const members = await roomApi.getGroupMembers(roomId.value)
groupMembers.value = members
// 构建用户缓存
members.forEach(member => {
if (member.user_id) {
@@ -420,11 +420,11 @@ async function loadGroupMembers() {
}
}
})
console.log('📋 群成员数据:', members.length, '人,用户缓存:', userCache.value.size, '人')
} catch (e) {
console.error('加载群成员失败:', e)
}
} catch (e) {
console.error('加载群成员失败:', e)
}
}
async function loadMessages() { if (!roomId.value) return; try { const cached = chatStore.getRoomMessages(roomId.value); if (cached.length > 0) { messages.value = cached.map((m) => ({ ...m, isSelf: m.sender_user_id === currentUser.value?.id, extra: typeof m.extra === 'string' ? JSON.parse(m.extra || '{}') : m.extra })); scrollToBottom(false); return } const res = await messageApi.getMessages(roomId.value, 1, 50); messages.value = (res.data || []).reverse().map((m: ChatMessage) => ({ ...m, isSelf: m.sender_user_id === currentUser.value?.id, extra: typeof m.extra === 'string' ? JSON.parse(m.extra || '{}') : m.extra })); chatStore.setRoomMessages(roomId.value, messages.value); hasMore.value = res.data.length >= 50; scrollToBottom(false) } catch (error) { console.error('加载消息失败:', error) } }
function onScrollToUpper() { if (!loadingMore.value && hasMore.value) { loadMoreMessages() } }
@@ -434,12 +434,12 @@ function startAudioCall() { if (!targetId.value) { toast.show('无法发起通
function startVideoCall() { if (!targetId.value) { toast.show('无法发起通话'); return } webrtc.startCall('video', targetId.value, roomId.value, targetUser.value?.name, targetUser.value?.avatar) }
function onMoreAudioCall() { showMore.value = false; startAudioCall() }
function onMoreVideoCall() { showMore.value = false; startVideoCall() }
function handleNewMessage(msg: ChatMessage) {
function handleNewMessage(msg: ChatMessage) {
if (msg.message_type === 6) return
const parsedExtra = typeof msg.extra === 'string' ? JSON.parse(msg.extra || '{}') : msg.extra
const newMsg: ChatMessage = { ...msg, isSelf: msg.sender_user_id === currentUser.value?.id, extra: parsedExtra }
const isCurrentChat = msg.room_id === roomId.value
// 尝试从消息中提取发送者信息并缓存
const msgAny = msg as any
if (msg.sender_user_id && !userCache.value.has(msg.sender_user_id)) {
@@ -449,18 +449,18 @@ function handleNewMessage(msg: ChatMessage) {
userCache.value.set(msg.sender_user_id, { name: senderName, avatar: senderAvatar })
}
}
conversationStore.handleMessageUpdate(newMsg, newMsg.isSelf || false, isCurrentChat)
if (!isCurrentChat) return
const exists = messages.value.some(
(m) => m.id === newMsg.id || (m.isSelf && m.content === newMsg.content && Math.abs(new Date(m.created_at).getTime() - new Date(newMsg.created_at).getTime()) < 2000)
const exists = messages.value.some(
(m) => m.id === newMsg.id || (m.isSelf && m.content === newMsg.content && Math.abs(new Date(m.created_at).getTime() - new Date(newMsg.created_at).getTime()) < 2000)
)
if (!exists) {
if (!exists) {
messages.value.push(newMsg)
chatStore.addMessage(roomId.value, newMsg)
scrollToBottom(true)
}
scrollToBottom(true)
}
}
function scrollToBottom(smooth = true) { nextTick(() => { if (messages.value.length > 0) { scrollWithAnimation.value = smooth; scrollToId.value = `msg-${messages.value[messages.value.length - 1].id}` } }) }
async function sendTextMessage() { if (!inputText.value.trim() || !roomId.value) return; const content = inputText.value.trim(); inputText.value = ''; const message: ChatMessage = { id: Date.now(), room_id: roomId.value, sender_user_id: currentUser.value!.id, receiver_user_id: targetId.value || '', message_type: 0, content, duration: 0, extra: {}, created_at: new Date().toISOString(), isSelf: true }; messages.value.push(message); chatStore.addMessage(roomId.value, message); conversationStore.handleMessageUpdate(message, true, true); scrollToBottom(); try { await messageApi.sendMessage({ sender_client_id: wsManager.getClientId() || '', receiver_user_id: targetId.value || '', room_id: roomId.value, message_type: 0, content, extra: JSON.stringify({}) }) } catch { const index = messages.value.findIndex((m) => m.id === message.id); if (index > -1) { messages.value.splice(index, 1) } toast.error('发送失败'); inputText.value = content } }
@@ -487,33 +487,33 @@ function getFileSize(msg: ChatMessage): number { if (typeof msg.extra === 'strin
function getMessageSenderAvatar(msg: ChatMessage): string {
const msgAny = msg as any
const senderId = msg.sender_user_id
// 1. 尝试从消息本身获取发送者头像
if (msgAny.sender?.avatar) return resolveImageUrl(msgAny.sender.avatar)
if (msgAny.sender_avatar) return resolveImageUrl(msgAny.sender_avatar)
if (msgAny.from_user?.avatar) return resolveImageUrl(msgAny.from_user.avatar)
// 2. 群聊时获取头像
if (isGroupChat.value && senderId) {
// 优先从用户缓存获取
const cachedUser = userCache.value.get(senderId)
if (cachedUser?.avatar) return resolveImageUrl(cachedUser.avatar)
// 从群成员列表获取
const member = groupMembers.value.find(m => m.user_id === senderId)
if (member) {
const avatar = member.user?.avatar || (member as any).avatar || (member as any).user_avatar
if (avatar) return resolveImageUrl(avatar)
}
// 从联系人列表中查找
const contact = chatStore.contacts.find(c => c.contact_user_id === senderId)
if (contact?.user?.avatar) return resolveImageUrl(contact.user.avatar)
// 返回空字符串,让 AppAvatar 组件显示首字母头像
return ''
}
// 3. 单聊时使用对方头像
return resolveImageUrl(targetUser.value?.avatar || '')
}
@@ -521,33 +521,33 @@ function getMessageSenderAvatar(msg: ChatMessage): string {
function getMessageSenderName(msg: ChatMessage): string {
const msgAny = msg as any
const senderId = msg.sender_user_id
// 1. 尝试从消息本身获取发送者名称
if (msgAny.sender?.name) return msgAny.sender.name
if (msgAny.sender_name) return msgAny.sender_name
if (msgAny.from_user?.name) return msgAny.from_user.name
// 2. 群聊时获取名称
if (isGroupChat.value && senderId) {
// 优先从用户缓存获取
const cachedUser = userCache.value.get(senderId)
if (cachedUser?.name) return cachedUser.name
// 从群成员列表获取
const member = groupMembers.value.find(m => m.user_id === senderId)
if (member) {
const name = member.nickname || member.user?.name || (member as any).name || (member as any).user_name
if (name) return name
}
// 从联系人列表中查找
const contact = chatStore.contacts.find(c => c.contact_user_id === senderId)
if (contact?.user?.name) return contact.remark_name || contact.user.name
// 返回用户 ID 的后 4 位作为备用名称
return `用户${senderId.slice(-4)}`
}
// 3. 单聊时使用对方名称
return targetUser.value?.name || chatName.value || '未知'
}
@@ -652,54 +652,54 @@ async function checkRecordPermission(): Promise<boolean> {
})
}
function initRecorderManager() {
function initRecorderManager() {
if (recorderManager) return recorderManager
// H5 平台不支持 uni.getRecorderManager
/* #ifdef H5 */
toast.error('当前平台不支持录音')
return null
/* #endif */
/* #ifndef H5 */
const manager = uni.getRecorderManager()
if (!manager) {
toast.error('录音功能不可用')
return null
}
manager.onStart(() => {
manager.onStart(() => {
console.log('录音开始')
isRecording.value = true
recordDuration.value = 0
recordTimer = setInterval(() => {
recordTimer = setInterval(() => {
recordDuration.value++
if (recordDuration.value >= 60) {
stopRecording()
}
}, 1000)
if (recordDuration.value >= 60) {
stopRecording()
}
}, 1000)
})
manager.onStop((res) => {
manager.onStop((res) => {
console.log('录音结束', res)
clearRecordTimer()
isRecording.value = false
if (!isCancelRecording.value && res.tempFilePath && recordDuration.value >= 1) {
sendVoiceMessage(res.tempFilePath, recordDuration.value)
}
if (!isCancelRecording.value && res.tempFilePath && recordDuration.value >= 1) {
sendVoiceMessage(res.tempFilePath, recordDuration.value)
}
isCancelRecording.value = false
recordDuration.value = 0
recordDuration.value = 0
})
manager.onError((err) => {
manager.onError((err) => {
console.error('录音错误', err)
clearRecordTimer()
isRecording.value = false
isCancelRecording.value = false
toast.error('录音失败:' + (err.errMsg || '未知错误'))
})
recorderManager = manager
return recorderManager
/* #endif */
@@ -707,80 +707,80 @@ function initRecorderManager() {
function clearRecordTimer() { if (recordTimer) { clearInterval(recordTimer); recordTimer = null } }
async function startRecording(e: TouchEvent) {
async function startRecording(e: TouchEvent) {
recordStartY = e.touches[0].clientY
isCancelRecording.value = false
// 先检查权限
const hasPermission = await checkRecordPermission()
if (!hasPermission) {
toast.error('无录音权限')
return
}
const recorder = initRecorderManager()
if (!recorder) {
return
}
recorder.start({
duration: 60000,
sampleRate: 16000,
numberOfChannels: 1,
encodeBitRate: 48000,
recorder.start({
duration: 60000,
sampleRate: 16000,
numberOfChannels: 1,
encodeBitRate: 48000,
format: 'aac' // 使用 aac 格式,跨平台兼容性更好
})
})
}
function onRecordingMove(e: TouchEvent) { if (!isRecording.value) return; const currentY = e.touches[0].clientY; const diff = recordStartY - currentY; isCancelRecording.value = diff > 80 }
function stopRecording() { if (!isRecording.value) return; if (recorderManager) { recorderManager.stop() } }
function cancelRecording() { isCancelRecording.value = true; stopRecording() }
async function sendVoiceMessage(filePath: string, duration: number) {
async function sendVoiceMessage(filePath: string, duration: number) {
toast.loading('发送中...')
try {
try {
const attachment = await attachmentApi.uploadAttachment(filePath, 'audio')
toast.close()
const extra: Record<string, any> = {
url: attachment.file_url,
const extra: Record<string, any> = {
url: attachment.file_url,
name: attachment.file_name || 'voice.m4a', // 使用 m4a 扩展名aac 格式)
size: attachment.file_size || 0,
duration: duration,
attachment_id: attachment.id
size: attachment.file_size || 0,
duration: duration,
attachment_id: attachment.id
}
const message: ChatMessage = {
id: Date.now(),
room_id: roomId.value,
sender_user_id: currentUser.value!.id,
receiver_user_id: targetId.value || '',
message_type: 2,
content: attachment.file_url,
duration: duration,
extra,
created_at: new Date().toISOString(),
isSelf: true
const message: ChatMessage = {
id: Date.now(),
room_id: roomId.value,
sender_user_id: currentUser.value!.id,
receiver_user_id: targetId.value || '',
message_type: 2,
content: attachment.file_url,
duration: duration,
extra,
created_at: new Date().toISOString(),
isSelf: true
}
messages.value.push(message)
chatStore.addMessage(roomId.value, message)
conversationStore.handleMessageUpdate(message, true, true)
scrollToBottom(true)
await messageApi.sendMessage({
sender_client_id: wsManager.getClientId() || '',
receiver_user_id: targetId.value || '',
room_id: roomId.value,
message_type: 2,
content: attachment.file_url,
duration: duration,
extra: JSON.stringify(extra)
await messageApi.sendMessage({
sender_client_id: wsManager.getClientId() || '',
receiver_user_id: targetId.value || '',
room_id: roomId.value,
message_type: 2,
content: attachment.file_url,
duration: duration,
extra: JSON.stringify(extra)
})
toast.success('发送成功')
} catch (error: any) {
toast.success('发送成功')
} catch (error: any) {
toast.close()
console.error('语音发送失败:', error)
toast.error(error.message || '发送失败')
}
toast.error(error.message || '发送失败')
}
}
</script>
@@ -840,12 +840,12 @@ async function sendVoiceMessage(filePath: string, duration: number) {
background: rgba(255, 255, 255, 0.9); // 设计稿 bg-white/90
backdrop-filter: blur(12px); // 设计稿 backdrop-blur-md
-webkit-backdrop-filter: blur(12px);
border-bottom: 2rpx solid #f3f4f6; // 设计稿 border-gray-100
border-bottom: 2rpx solid transparent; // 设计稿 border-transparent
transition: all 0.3s;
.theme-dark & {
background: rgba(28, 25, 23, 0.9); // 设计稿 dark:bg-warm-900/90
border-color: #292524; // 设计稿 dark:border-warm-800
border-color: rgba(68, 64, 60, 0.5); // 设计稿 dark:border-warm-800/50
}
}
@@ -872,7 +872,7 @@ async function sendVoiceMessage(filePath: string, duration: number) {
width: 56rpx; // 设计稿 w-7 = 28px = 56rpx
height: 56rpx;
color: #1f2937; // 设计稿 text-gray-800
.theme-dark & {
color: #f5f5f4; // 设计稿 dark:text-warm-100
}
@@ -888,7 +888,7 @@ async function sendVoiceMessage(filePath: string, duration: number) {
font-weight: 700; // 设计稿 font-bold
color: #111827; // 设计稿 text-gray-900
line-height: 1.2; // 设计稿 leading-tight
.theme-dark & {
color: #f5f5f4; // 设计稿 dark:text-warm-100
}
@@ -902,7 +902,7 @@ async function sendVoiceMessage(filePath: string, duration: number) {
color: #9ca3af; // 设计稿 text-gray-400
font-weight: 500; // 设计稿 font-medium
margin-top: 2rpx;
.theme-dark & {
color: #a8a29e; // 设计稿 dark:text-warm-400
}
@@ -937,7 +937,7 @@ async function sendVoiceMessage(filePath: string, duration: number) {
height: 48rpx;
color: #4b5563; // 设计稿 text-gray-600
transition: color 0.2s ease;
.theme-dark & {
color: #a8a29e; // 设计稿 dark:text-warm-400
}
@@ -946,7 +946,7 @@ async function sendVoiceMessage(filePath: string, duration: number) {
&:hover svg {
color: #4f46e5; // 设计稿 hover:text-indigo-600
}
.theme-dark &:hover svg {
color: #fb923c; // 设计稿 dark:hover:text-orange-400
}
@@ -960,7 +960,7 @@ async function sendVoiceMessage(filePath: string, duration: number) {
overflow: hidden;
box-sizing: border-box;
background: #F5F7FA; // 设计稿 bg-[#F5F7FA]
.theme-dark & {
background: #121212; // 设计稿 dark:bg-[#121212]
}
@@ -997,7 +997,7 @@ async function sendVoiceMessage(filePath: string, duration: number) {
color: #9ca3af; // 设计稿 text-gray-400
transition: background 0.3s, color 0.3s;
.theme-dark & {
.theme-dark & {
background: rgba(41, 37, 36, 0.5); // 设计稿 bg-warm-800/50
color: #78716c; // 设计稿 text-warm-600
}
@@ -1034,17 +1034,17 @@ async function sendVoiceMessage(filePath: string, duration: number) {
border-radius: 24rpx; // 设计稿 rounded-xl = 12px = 24rpx
overflow: hidden;
background: #e5e7eb; // 设计稿 bg-gray-200
.theme-dark & {
background: #292524; // 设计稿 dark:bg-warm-800
}
.avatar-img {
width: 100%;
height: 100%;
border-radius: 24rpx;
}
.avatar-placeholder {
width: 100%;
height: 100%;
@@ -1076,7 +1076,7 @@ async function sendVoiceMessage(filePath: string, duration: number) {
color: #9ca3af; // 设计稿 text-gray-400
margin-bottom: 8rpx; // 设计稿 mb-1 = 4px = 8rpx
margin-left: 8rpx; // 设计稿 ml-1 = 4px = 8rpx
.theme-dark & {
color: #78716c; // 设计稿 dark:text-warm-500
}
@@ -1145,7 +1145,7 @@ async function sendVoiceMessage(filePath: string, duration: number) {
border: 2rpx solid #44403c !important;
}
}
// 自己发送的语音
.is-self &.audio {
background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%) !important;
@@ -1169,7 +1169,7 @@ async function sendVoiceMessage(filePath: string, duration: number) {
display: block;
background: #e5e7eb; // 设计稿 bg-gray-200
border: 2rpx solid #f3f4f6; // 设计稿 border-gray-100
.theme-dark & {
background: #292524; // 设计稿 dark:bg-warm-800
border-color: #44403c; // 设计稿 dark:border-warm-700
@@ -1185,7 +1185,7 @@ async function sendVoiceMessage(filePath: string, duration: number) {
gap: 24rpx; // 设计稿 gap-3 = 12px = 24rpx
min-width: 200rpx; // 设计稿 min-w-[100px] = 200rpx
color: #6b7280; // 设计稿 text-gray-500
.theme-dark & {
color: #a8a29e; // 设计稿 dark:text-warm-400
}
@@ -1289,27 +1289,27 @@ async function sendVoiceMessage(filePath: string, duration: number) {
width: 40rpx; // 设计稿 w-5 = 20px = 40rpx
height: 40rpx;
}
.theme-dark & {
background: rgba(154, 52, 18, 0.3); // 设计稿 dark:bg-orange-900/30
}
}
// 自己发送的文件 - 设计稿: bg-white/20 text-white
&.file-self {
background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%);
box-shadow: 0 4rpx 16rpx rgba(79, 70, 229, 0.2);
border: none;
.theme-dark & {
border: none;
}
.file-icon-box.icon-self {
background: rgba(255, 255, 255, 0.2); // 设计稿 bg-white/20
color: #fff; // 设计稿 text-white
}
.file-meta {
.fname {
color: #fff; // 设计稿 text-white
@@ -1335,7 +1335,7 @@ async function sendVoiceMessage(filePath: string, duration: number) {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
.theme-dark & {
color: #f5f5f4; // 设计稿 dark:text-warm-100
}
@@ -1345,7 +1345,7 @@ async function sendVoiceMessage(filePath: string, duration: number) {
.fsize {
font-size: 20rpx; // 设计稿 text-[10px]
color: #9ca3af; // 设计稿 text-gray-400
.theme-dark & {
color: #78716c; // 设计稿 dark:text-warm-500
}
@@ -1362,7 +1362,7 @@ async function sendVoiceMessage(filePath: string, duration: number) {
overflow: hidden;
background: #000;
cursor: pointer;
.video-thumb {
width: 100%;
height: auto;
@@ -1371,12 +1371,12 @@ async function sendVoiceMessage(filePath: string, duration: number) {
opacity: 0.9;
transition: opacity 0.2s;
}
&:hover .video-thumb,
&:active .video-thumb {
opacity: 1;
}
.play-overlay {
position: absolute;
inset: 0;
@@ -1384,7 +1384,7 @@ async function sendVoiceMessage(filePath: string, duration: number) {
align-items: center;
justify-content: center;
}
.play-btn-circle {
width: 80rpx; // 设计稿 w-10 = 40px = 80rpx
height: 80rpx;
@@ -1396,7 +1396,7 @@ async function sendVoiceMessage(filePath: string, duration: number) {
align-items: center;
justify-content: center;
border: 2rpx solid rgba(255, 255, 255, 0.3);
svg {
width: 32rpx; // 设计稿 w-4 = 16px = 32rpx
height: 32rpx;
@@ -1404,7 +1404,7 @@ async function sendVoiceMessage(filePath: string, duration: number) {
margin-left: 4rpx; // 视觉居中调整
}
}
.video-duration {
position: absolute;
bottom: 16rpx;
@@ -1418,7 +1418,7 @@ async function sendVoiceMessage(filePath: string, duration: number) {
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
}
// 兼容旧的 video 标签
.video-player {
width: 100%;
@@ -1463,16 +1463,16 @@ async function sendVoiceMessage(filePath: string, duration: number) {
background: rgba(255, 255, 255, 0.95);
border-radius: 32rpx;
min-width: 300rpx;
.theme-dark & {
background: rgba(41, 37, 36, 0.95);
}
&.cancel-mode {
.recording-icon {
background: #ef4444;
}
.wave-bars .wave-bar {
background: #ef4444;
animation: none;
@@ -1491,7 +1491,7 @@ async function sendVoiceMessage(filePath: string, duration: number) {
justify-content: center;
margin-bottom: 32rpx;
transition: background 0.2s;
svg {
width: 60rpx;
height: 60rpx;
@@ -1514,7 +1514,7 @@ async function sendVoiceMessage(filePath: string, duration: number) {
background: #10b981;
border-radius: 8rpx;
animation: wave 0.8s infinite ease-in-out;
@for $i from 1 through 5 {
&:nth-child(#{$i}) {
animation-delay: #{($i - 1) * 0.1}s;
@@ -1553,7 +1553,7 @@ async function sendVoiceMessage(filePath: string, duration: number) {
padding: 24rpx 32rpx 64rpx; // 设计稿 px-4 py-3 pb-8
padding-bottom: calc(64rpx + env(safe-area-inset-bottom));
transition: all 0.3s;
.theme-dark & {
background: #1c1917; // 设计稿 dark:bg-warm-900
border-color: #292524; // 设计稿 dark:border-warm-800
@@ -1584,7 +1584,7 @@ async function sendVoiceMessage(filePath: string, duration: number) {
width: 56rpx;
height: 56rpx;
}
.theme-dark & {
color: #a8a29e; // 设计稿 dark:text-warm-400
}
@@ -1592,7 +1592,7 @@ async function sendVoiceMessage(filePath: string, duration: number) {
&:hover {
color: #4f46e5; // 设计稿 hover:text-indigo-600
}
.theme-dark &:hover {
color: #fb923c; // 设计稿 dark:hover:text-orange-400
}
@@ -1632,7 +1632,7 @@ async function sendVoiceMessage(filePath: string, duration: number) {
line-height: 1.5;
color: #1f2937; // 设计稿 text-gray-800
background: transparent;
.theme-dark & {
color: #f5f5f4; // 设计稿 dark:text-warm-100
}
@@ -1640,7 +1640,7 @@ async function sendVoiceMessage(filePath: string, duration: number) {
.input-placeholder {
color: #9ca3af; // 设计稿 placeholder-gray-400
.theme-dark & {
color: #78716c; // 设计稿 dark:placeholder-warm-500
}

View File

@@ -618,6 +618,7 @@ function goUserDetail(user: User) {
border-radius: 40rpx;
transition: all 0.15s;
box-shadow: 0 4rpx 30rpx rgba(0, 0, 0, 0.05);
overflow: hidden;
.dark & {
box-shadow: none;
@@ -633,6 +634,7 @@ function goUserDetail(user: User) {
.user-info {
flex: 1;
min-width: 0;
margin-left: 24rpx;
.user-name {
@@ -640,6 +642,9 @@ function goUserDetail(user: User) {
font-weight: 700;
color: var(--text-main);
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.user-id {
@@ -647,11 +652,15 @@ function goUserDetail(user: User) {
color: var(--text-sub);
margin-top: 4rpx;
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.add-btn {
padding: 16rpx 40rpx;
flex-shrink: 0;
padding: 16rpx 32rpx;
border-radius: 40rpx;
font-size: 28rpx;
font-weight: 700;

View File

@@ -446,7 +446,10 @@ async function loadGroupInfo() {
// 过滤可邀请的好友
const memberIds = new Set(memberList.map(m => m.user_id))
availableContacts.value = contacts.filter(c => !memberIds.has(c.user_id || c.id))
availableContacts.value = (contacts || []).filter(c => {
const contactUserId = c.user_id || c.contact_user_id || c.id
return contactUserId && !memberIds.has(contactUserId)
})
} catch (error) {
console.error('加载群信息失败:', error)
toast.error('加载失败')

View File

@@ -620,11 +620,7 @@ function getImageSize(moment: Moment): string {
height: 0;
border-left: 12rpx solid transparent;
border-right: 12rpx solid transparent;
border-bottom: 12rpx solid #f3f4f6; // gray-100
.dark & {
border-bottom-color: #292524; // 设计稿: warm-800
}
border-bottom: 12rpx solid var(--bg-detail);
}
.likes-list {
@@ -671,6 +667,12 @@ function getImageSize(moment: Moment): string {
}
}
// FAB 脉冲动画
@keyframes fab-pulse {
0%, 100% { box-shadow: 0 10rpx 30rpx rgba(16, 185, 129, 0.3); }
50% { box-shadow: 0 10rpx 40rpx rgba(16, 185, 129, 0.5); }
}
// 悬浮发布按钮
.fab-btn {
position: fixed;
@@ -686,6 +688,7 @@ function getImageSize(moment: Moment): string {
justify-content: center;
z-index: 99;
transition: transform 0.2s;
animation: fab-pulse 2s ease-in-out infinite;
svg {
width: 52rpx;
@@ -694,6 +697,7 @@ function getImageSize(moment: Moment): string {
&:active {
transform: scale(0.95);
animation: none;
}
}
</style>