UI设计优化
This commit is contained in:
@@ -1,29 +1,80 @@
|
||||
<template>
|
||||
<view class="message-bubble" :class="{ 'is-self': isSelf }">
|
||||
<view class="msg-row" :class="{ 'is-self': isSelf }">
|
||||
<!-- 头像 -->
|
||||
<view v-if="!isSelf" class="bubble-avatar">
|
||||
<view class="avatar-wrap" @click="$emit('avatar-click', msg.sender_user_id)">
|
||||
<app-avatar
|
||||
:src="avatarUrl"
|
||||
:name="avatarName"
|
||||
:size="72"
|
||||
radius="8rpx"
|
||||
:src="avatar"
|
||||
:name="avatarName"
|
||||
:size="80"
|
||||
radius="24rpx"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 消息内容 -->
|
||||
<view class="bubble-content" :class="bubbleClass">
|
||||
<slot />
|
||||
</view>
|
||||
<!-- 内容列 -->
|
||||
<view class="content-col" :class="{ 'items-end': isSelf }">
|
||||
<!-- 群聊昵称 -->
|
||||
<text v-if="senderName" class="sender-name">{{ senderName }}</text>
|
||||
|
||||
<!-- 自己的头像 -->
|
||||
<view v-if="isSelf" class="bubble-avatar">
|
||||
<app-avatar
|
||||
:src="selfAvatarUrl"
|
||||
:name="selfAvatarName"
|
||||
:size="72"
|
||||
radius="8rpx"
|
||||
bg-color="#10aeff"
|
||||
/>
|
||||
<!-- 气泡实体 -->
|
||||
<view
|
||||
class="bubble"
|
||||
:class="[
|
||||
isSelf ? 'bubble-self' : 'bubble-other',
|
||||
mediaTypeClass
|
||||
]"
|
||||
@longpress="$emit('long-press', msg)"
|
||||
>
|
||||
<!-- 0. 纯文本 -->
|
||||
<text v-if="msg.message_type === 0" class="msg-text" selectable>{{ msg.content }}</text>
|
||||
|
||||
<!-- 1. 图片 -->
|
||||
<view v-else-if="msg.message_type === 1" class="image-bubble">
|
||||
<image
|
||||
:src="getMediaUrl(msg)"
|
||||
mode="widthFix"
|
||||
class="msg-image"
|
||||
@click="$emit('preview-image', getMediaUrl(msg))"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 2. 语音 -->
|
||||
<view v-else-if="msg.message_type === 2" class="audio-capsule" @click="$emit('play-audio', msg)">
|
||||
<view class="play-icon-box">
|
||||
<svg v-if="playingAudioId !== msg.id" viewBox="0 0 24 24" fill="currentColor" class="play-icon"><path d="M12 2a10 10 0 0 0-10 10 10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2zm-2 14.5v-9l6 4.5-6 4.5z"/></svg>
|
||||
<svg v-else viewBox="0 0 24 24" fill="currentColor" class="play-icon"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>
|
||||
</view>
|
||||
<view class="wave-visualizer">
|
||||
<view class="wave-bar" :class="{ animating: playingAudioId === msg.id }"></view>
|
||||
<view class="wave-bar" :class="{ animating: playingAudioId === msg.id }"></view>
|
||||
<view class="wave-bar" :class="{ animating: playingAudioId === msg.id }"></view>
|
||||
<view class="wave-bar" :class="{ animating: playingAudioId === msg.id }"></view>
|
||||
</view>
|
||||
<text class="duration-tag">{{ msg.duration || 0 }}"</text>
|
||||
</view>
|
||||
|
||||
<!-- 3. 视频 -->
|
||||
<view v-else-if="msg.message_type === 3" class="video-preview" @click="$emit('play-video', getMediaUrl(msg))">
|
||||
<image :src="getVideoThumb(msg)" mode="aspectFill" class="video-thumb" />
|
||||
<view class="play-overlay">
|
||||
<view class="play-btn-circle">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" class="icon-play"><polygon points="5 3 19 12 5 21 5 3"/></svg>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 8. 文件 -->
|
||||
<view v-else-if="msg.message_type === 8" class="file-card" :class="{ 'file-self': isSelf }" @click="$emit('open-file', getMediaUrl(msg))">
|
||||
<view class="file-icon-box" :class="{ 'icon-self': isSelf }">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
|
||||
</view>
|
||||
<view class="file-meta">
|
||||
<text class="fname text-ellipsis">{{ getFileName(msg) }}</text>
|
||||
<text class="fsize">{{ formatSize(getFileSize(msg)) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<text v-else class="msg-text">[不支持的消息类型]</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -31,90 +82,147 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import AppAvatar from '@/components/common/AppAvatar.vue'
|
||||
import { resolveImageUrl } from '@/utils/image'
|
||||
import { formatSize } from '@/utils/format'
|
||||
import type { ChatMessage } from '@/types/api'
|
||||
|
||||
interface Props {
|
||||
isSelf?: boolean
|
||||
avatarUrl?: string
|
||||
avatarName?: string
|
||||
selfAvatarUrl?: string
|
||||
selfAvatarName?: string
|
||||
type?: 'text' | 'image' | 'audio' | 'video' | 'file'
|
||||
const props = defineProps<{
|
||||
msg: ChatMessage
|
||||
isSelf: boolean
|
||||
senderName?: string
|
||||
avatar?: string
|
||||
playingAudioId?: number | null
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
(e: 'avatar-click', id?: string): void
|
||||
(e: 'play-audio', msg: ChatMessage): void
|
||||
(e: 'preview-image', url: string): void
|
||||
(e: 'play-video', url: string): void
|
||||
(e: 'open-file', url: string): void
|
||||
(e: 'long-press', msg: ChatMessage): void
|
||||
}>()
|
||||
|
||||
const avatarName = computed(() => props.senderName || 'U')
|
||||
|
||||
const mediaTypeClass = computed(() => {
|
||||
const type = props.msg.message_type
|
||||
if (type === 1) return 'media-bubble image'
|
||||
if (type === 2) return 'media-bubble audio'
|
||||
if (type === 3) return 'media-bubble video'
|
||||
if (type === 8) return 'media-bubble file'
|
||||
return ''
|
||||
})
|
||||
|
||||
// Helpers (复用原 index.vue 逻辑)
|
||||
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)
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
isSelf: false,
|
||||
avatarUrl: '',
|
||||
avatarName: '',
|
||||
selfAvatarUrl: '',
|
||||
selfAvatarName: '',
|
||||
type: 'text'
|
||||
})
|
||||
function getFileName(msg: ChatMessage) {
|
||||
const extra = typeof msg.extra === 'string' ? JSON.parse(msg.extra || '{}') : msg.extra
|
||||
return extra?.name || '未知文件'
|
||||
}
|
||||
|
||||
const bubbleClass = computed(() => {
|
||||
const classes = []
|
||||
if (props.isSelf) {
|
||||
classes.push('bubble-self')
|
||||
} else {
|
||||
classes.push('bubble-other')
|
||||
}
|
||||
if (props.type !== 'text') {
|
||||
classes.push('bubble-media')
|
||||
}
|
||||
return classes
|
||||
})
|
||||
function getFileSize(msg: ChatMessage) {
|
||||
const extra = typeof msg.extra === 'string' ? JSON.parse(msg.extra || '{}') : msg.extra
|
||||
return extra?.size || 0
|
||||
}
|
||||
|
||||
function getVideoThumb(msg: ChatMessage) {
|
||||
const extra = typeof msg.extra === 'string' ? JSON.parse(msg.extra || '{}') : msg.extra
|
||||
if (extra?.thumb || extra?.cover) return resolveImageUrl(extra.thumb || extra.cover)
|
||||
return getMediaUrl(msg)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.message-bubble {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 24rpx;
|
||||
/* * 气泡组件样式
|
||||
* 完全复用原 index.vue 的 .msg-row, .bubble 及其子元素样式
|
||||
*/
|
||||
|
||||
&.is-self {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
/* 布局 */
|
||||
.msg-row {
|
||||
display: flex; align-items: flex-start; gap: 20rpx;
|
||||
&.is-self { flex-direction: row-reverse; }
|
||||
}
|
||||
.avatar-wrap { width: 80rpx; height: 80rpx; flex-shrink: 0; border-radius: 24rpx; box-shadow: 0 4rpx 8rpx rgba(0,0,0,0.05); }
|
||||
.content-col {
|
||||
max-width: 72%; display: flex; flex-direction: column;
|
||||
&.items-end { align-items: flex-end; }
|
||||
.sender-name { font-size: 20rpx; color: #9ca3af; margin-bottom: 8rpx; margin-left: 8rpx; .theme-dark & { color: #78716c; } }
|
||||
}
|
||||
|
||||
.bubble-avatar {
|
||||
margin: 0 16rpx;
|
||||
}
|
||||
|
||||
.bubble-content {
|
||||
max-width: 60%;
|
||||
padding: 20rpx 24rpx;
|
||||
border-radius: 16rpx;
|
||||
word-break: break-all;
|
||||
font-size: 30rpx;
|
||||
line-height: 1.6;
|
||||
|
||||
&.bubble-self {
|
||||
background: var(--bubble-self);
|
||||
color: var(--text-primary);
|
||||
border-top-right-radius: 4rpx;
|
||||
}
|
||||
/* 核心气泡 */
|
||||
.bubble {
|
||||
padding: 24rpx; border-radius: 32rpx; font-size: 30rpx; line-height: 1.6; position: relative; word-break: break-all; transition: all 0.3s ease;
|
||||
|
||||
/* 对方消息 */
|
||||
&.bubble-other {
|
||||
background: var(--bubble-other);
|
||||
color: var(--text-primary);
|
||||
border-top-left-radius: 4rpx;
|
||||
background: #ffffff; color: #1f2937; border-bottom-left-radius: 8rpx; box-shadow: 0 2rpx 4rpx rgba(0,0,0,0.05);
|
||||
.theme-dark & { background: #262626; color: #e7e5e4; border: 2rpx solid #292524; box-shadow: none; }
|
||||
}
|
||||
|
||||
&.bubble-media {
|
||||
padding: 8rpx;
|
||||
background: transparent;
|
||||
/* 自己消息 */
|
||||
&.bubble-self {
|
||||
background: linear-gradient(135deg, #6366F1, #4F46E5); color: #ffffff; border-bottom-right-radius: 8rpx; box-shadow: 0 4rpx 16rpx rgba(79, 70, 229, 0.2);
|
||||
.theme-dark & { background: linear-gradient(135deg, #fb923c, #ea580c); box-shadow: 0 4rpx 16rpx rgba(234, 88, 12, 0.3); }
|
||||
}
|
||||
|
||||
/* 媒体容器 */
|
||||
&.media-bubble { padding: 0; background: transparent !important; box-shadow: none !important; border: none !important; }
|
||||
}
|
||||
|
||||
/* 媒体类型 */
|
||||
.image-bubble .msg-image { width: 360rpx; border-radius: 32rpx; background: #f3f4f6; border: 2rpx solid #f3f4f6; .theme-dark & { background: #292524; border-color: #292524; } }
|
||||
|
||||
/* 语音胶囊 */
|
||||
.audio-capsule {
|
||||
display: flex; align-items: center; gap: 20rpx; min-width: 180rpx; padding: 24rpx; background: #ffffff; border-radius: 32rpx;
|
||||
.theme-dark & { background: #262626; border: 2rpx solid #292524; }
|
||||
|
||||
/* 自己的语音 */
|
||||
.is-self & { background: linear-gradient(135deg, #6366F1, #4F46E5) !important; border: none !important; .theme-dark & { background: linear-gradient(135deg, #fb923c, #ea580c) !important; } }
|
||||
|
||||
.play-icon { width: 32rpx; height: 32rpx; color: #6b7280; .theme-dark & { color: #a8a29e; } .is-self & { color: rgba(255,255,255,0.9); } }
|
||||
|
||||
.wave-visualizer {
|
||||
display: flex; align-items: flex-end; gap: 6rpx; height: 24rpx;
|
||||
.wave-bar {
|
||||
width: 6rpx; background: #9ca3af; border-radius: 4rpx; height: 8rpx;
|
||||
.theme-dark & { background: #78716c; } .is-self & { background: rgba(255,255,255,0.6); }
|
||||
&.animating { animation: wave 0.8s infinite ease-in-out; }
|
||||
}
|
||||
}
|
||||
.duration-tag { font-size: 26rpx; font-weight: 500; margin-left: auto; color: #6b7280; .theme-dark & { color: #a8a29e; } .is-self & { color: #fff; } }
|
||||
}
|
||||
@keyframes wave { 0%, 100% { height: 8rpx; } 50% { height: 24rpx; } }
|
||||
|
||||
/* 视频预览 */
|
||||
.video-preview {
|
||||
position: relative; width: 360rpx; height: 200rpx; border-radius: 32rpx; overflow: hidden; background: #000;
|
||||
.video-thumb { width: 100%; height: 100%; opacity: 0.9; }
|
||||
.play-overlay { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; }
|
||||
.play-btn-circle { width: 80rpx; height: 80rpx; background: rgba(0,0,0,0.3); backdrop-filter: blur(4px); border-radius: 50%; display: flex; align-items: center; justify-content: center; border: 2rpx solid rgba(255,255,255,0.3); color: #fff; svg { width: 32rpx; height: 32rpx; } }
|
||||
}
|
||||
|
||||
/* 文件卡片 */
|
||||
.file-card {
|
||||
background: #ffffff; padding: 24rpx; border-radius: 32rpx; display: flex; align-items: center; gap: 24rpx; min-width: 400rpx; border: 2rpx solid #f3f4f6;
|
||||
.theme-dark & { background: #262626; border-color: #292524; }
|
||||
|
||||
&.file-self { background: linear-gradient(135deg, #6366F1, #4F46E5); border: none; .theme-dark & { background: linear-gradient(135deg, #fb923c, #ea580c); } .f-name { color: #fff; } .f-size { color: rgba(255,255,255,0.7); } }
|
||||
|
||||
.file-icon-box {
|
||||
width: 80rpx; height: 80rpx; background: #ffedd5; border-radius: 16rpx; display: flex; align-items: center; justify-content: center; color: #f97316;
|
||||
.theme-dark & { background: rgba(154, 52, 18, 0.3); }
|
||||
&.icon-self { background: rgba(255,255,255,0.2); color: #fff; }
|
||||
svg { width: 40rpx; height: 40rpx; }
|
||||
}
|
||||
.file-meta { flex: 1; display: flex; flex-direction: column; overflow: hidden; .f-name { font-size: 28rpx; font-weight: 500; color: #1f2937; .theme-dark & { color: #e7e5e4; } } .f-size { font-size: 22rpx; color: #9ca3af; .theme-dark & { color: #78716c; } } }
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,51 +1,125 @@
|
||||
<template>
|
||||
<view class="message-input" :class="{ 'theme-dark': isDark }">
|
||||
<!-- @ 提及弹窗 (保持原有功能不变) -->
|
||||
<wd-popup v-model="showMentionPicker" position="bottom" safe-area-inset-bottom>
|
||||
<view class="mention-picker">
|
||||
<view class="mention-header">
|
||||
<text class="title">选择要 @ 的成员</text>
|
||||
<view class="close-btn" @click="showMentionPicker = false">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/>
|
||||
<line x1="6" y1="6" x2="18" y2="18"/>
|
||||
<view class="message-input-container" :class="{ 'theme-dark': isDark }">
|
||||
<!-- 悬浮输入条 -->
|
||||
<view class="input-floater">
|
||||
<!-- 1. @提醒选择器 -->
|
||||
<mention-picker
|
||||
v-if="isGroup"
|
||||
:show="showMentionPicker"
|
||||
:users="mentionUsers"
|
||||
@select="handleMentionSelect"
|
||||
@close="showMentionPicker = false"
|
||||
/>
|
||||
|
||||
<!-- 2. 表情面板 -->
|
||||
<emoji-picker
|
||||
:show="showEmoji"
|
||||
@select="handleEmojiSelect"
|
||||
@close="showEmoji = false"
|
||||
/>
|
||||
|
||||
<!-- 3. 核心输入栏 (Layout: Mic | Input | Smile | Action) -->
|
||||
<view class="input-bar-capsule">
|
||||
<!-- 左侧: 语音按钮 -->
|
||||
<view
|
||||
class="icon-btn"
|
||||
:class="{ 'recording': isRecording }"
|
||||
@touchstart="startRecording"
|
||||
@touchmove="onRecordingMove"
|
||||
@touchend="stopRecording"
|
||||
@touchcancel="cancelRecording"
|
||||
>
|
||||
<!-- Lucide Icon: Mic -->
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/>
|
||||
<path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
|
||||
<line x1="12" y1="19" x2="12" y2="23"/>
|
||||
<line x1="8" y1="23" x2="16" y2="23"/>
|
||||
</svg>
|
||||
</view>
|
||||
|
||||
<!-- 中间: 输入框 -->
|
||||
<view class="input-field-wrap">
|
||||
<textarea
|
||||
v-model="inputValue"
|
||||
class="text-input"
|
||||
:auto-height="true"
|
||||
:maxlength="-1"
|
||||
:cursor-spacing="20"
|
||||
placeholder="发送消息..."
|
||||
placeholder-class="input-placeholder"
|
||||
:show-confirm-bar="false"
|
||||
:adjust-position="false"
|
||||
@focus="$emit('focus')"
|
||||
@confirm="onSend"
|
||||
@input="onInput"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 右侧: 表情 & 更多/发送 -->
|
||||
<view class="right-actions">
|
||||
<!-- 表情按钮 -->
|
||||
<view class="icon-btn" @click="toggleEmoji">
|
||||
<!-- Lucide Icon: Smile -->
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<path d="M8 14s1.5 2 4 2 4-2 4-2"/>
|
||||
<line x1="9" y1="9" x2="9.01" y2="9"/>
|
||||
<line x1="15" y1="9" x2="15.01" y2="9"/>
|
||||
</svg>
|
||||
</view>
|
||||
|
||||
<!-- 发送按钮 (有文字时显示) -->
|
||||
<view v-if="inputValue.trim()" class="send-btn" @click="onSend">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="22" y1="2" x2="11" y2="13"/>
|
||||
<polygon points="22 2 15 22 11 13 2 9 22 2"/>
|
||||
</svg>
|
||||
</view>
|
||||
|
||||
<!-- 更多按钮 (无文字时显示) -->
|
||||
<view v-else class="icon-btn" @click="showMore = true">
|
||||
<!-- Lucide Icon: Plus Circle -->
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="12" y1="8" x2="12" y2="16"/>
|
||||
<line x1="8" y1="12" x2="16" y2="12"/>
|
||||
</svg>
|
||||
</view>
|
||||
</view>
|
||||
<scroll-view scroll-y class="mention-list">
|
||||
<view
|
||||
v-for="member in members"
|
||||
:key="member.user_id"
|
||||
class="mention-item"
|
||||
@click="selectMention(member)"
|
||||
>
|
||||
<app-avatar
|
||||
:src="member.user?.avatar"
|
||||
:name="member.nickname || member.user?.name"
|
||||
:size="72"
|
||||
radius="8rpx"
|
||||
/>
|
||||
<text class="mention-name">{{ member.nickname || member.user?.name || '未知' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 4. 更多功能面板 -->
|
||||
<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="emitAction('send-image')">
|
||||
<view class="tool-icon album">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<text>相册</text>
|
||||
</view>
|
||||
<view class="tool-item" @click="emitAction('send-camera')">
|
||||
<view class="tool-icon camera">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg>
|
||||
</view>
|
||||
<text>拍摄</text>
|
||||
</view>
|
||||
<view class="tool-item" @click="emitAction('send-file')">
|
||||
<view class="tool-icon file">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><polyline points="13 2 13 9 20 9"/></svg>
|
||||
</view>
|
||||
<text>文件</text>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
|
||||
<!-- 录音浮层 -->
|
||||
<!-- 5. 录音遮罩 (保持业务逻辑) -->
|
||||
<view v-if="isRecording" class="recording-overlay">
|
||||
<view class="recording-content" :class="{ 'cancel-mode': isCancelRecording }">
|
||||
<view class="recording-icon">
|
||||
<svg v-if="!isCancelRecording" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/>
|
||||
<path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
|
||||
<line x1="12" y1="19" x2="12" y2="23" stroke="currentColor" stroke-width="2"/>
|
||||
<line x1="8" y1="23" x2="16" y2="23" stroke="currentColor" stroke-width="2"/>
|
||||
</svg>
|
||||
<svg v-else viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="15" y1="9" x2="9" y2="15"/>
|
||||
<line x1="9" y1="9" x2="15" y2="15"/>
|
||||
</svg>
|
||||
<svg v-if="!isCancelRecording" viewBox="0 0 24 24" fill="currentColor"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23" stroke="currentColor" stroke-width="2"/><line x1="8" y1="23" x2="16" y2="23" stroke="currentColor" stroke-width="2"/></svg>
|
||||
<svg v-else viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>
|
||||
</view>
|
||||
<view class="wave-bars">
|
||||
<view class="wave-bar" v-for="i in 5" :key="i" :style="{ animationDelay: `${i * 0.1}s` }"></view>
|
||||
@@ -54,573 +128,299 @@
|
||||
<text class="recording-tip">{{ isCancelRecording ? '松开取消' : '上滑取消发送' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部输入栏 (按设计稿布局) -->
|
||||
<view class="input-bar">
|
||||
<!-- 左侧: 语音按钮 -->
|
||||
<view
|
||||
class="voice-btn"
|
||||
@touchstart="startRecording"
|
||||
@touchmove="onRecordingMove"
|
||||
@touchend="stopRecording"
|
||||
@touchcancel="cancelRecording"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/>
|
||||
<path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
|
||||
<line x1="12" y1="19" x2="12" y2="23"/>
|
||||
<line x1="8" y1="23" x2="16" y2="23"/>
|
||||
</svg>
|
||||
</view>
|
||||
|
||||
<!-- 中间: 输入框 -->
|
||||
<view class="input-wrap">
|
||||
<textarea
|
||||
v-model="inputValue"
|
||||
class="text-input"
|
||||
:auto-height="true"
|
||||
:maxlength="-1"
|
||||
placeholder="发送消息..."
|
||||
placeholder-class="input-placeholder"
|
||||
:adjust-position="true"
|
||||
@input="onInput"
|
||||
@confirm="onSend"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 右侧: 表情 + 加号/发送 -->
|
||||
<view class="right-actions">
|
||||
<view class="action-btn" @click="$emit('emoji')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<path d="M8 14s1.5 2 4 2 4-2 4-2"/>
|
||||
<line x1="9" y1="9" x2="9.01" y2="9"/>
|
||||
<line x1="15" y1="9" x2="15.01" y2="9"/>
|
||||
</svg>
|
||||
</view>
|
||||
|
||||
<!-- 有文字时显示发送按钮,否则显示加号 -->
|
||||
<view v-if="inputValue.trim()" class="send-btn" @click="onSend">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="22" y1="2" x2="11" y2="13"/>
|
||||
<polygon points="22 2 15 22 11 13 2 9 22 2"/>
|
||||
</svg>
|
||||
</view>
|
||||
<view v-else class="action-btn" @click="$emit('more')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="12" y1="8" x2="12" y2="16"/>
|
||||
<line x1="8" y1="12" x2="16" y2="12"/>
|
||||
</svg>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useTheme } from '@/composables/useTheme'
|
||||
import AppAvatar from '@/components/common/AppAvatar.vue'
|
||||
import EmojiPicker from '@/components/chat/EmojiPicker.vue'
|
||||
import MentionPicker from '@/components/chat/MentionPicker.vue'
|
||||
import type { MentionUser } from '@/components/chat/MentionPicker.vue'
|
||||
import type { GroupMember } from '@/types/api'
|
||||
|
||||
interface Props {
|
||||
// Props & Emits
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue?: string
|
||||
isGroup?: boolean
|
||||
members?: GroupMember[]
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
}>(), {
|
||||
modelValue: '',
|
||||
isGroup: false,
|
||||
members: () => []
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string): void
|
||||
(e: 'send', value: string): void
|
||||
(e: 'sendVoice', data: { path: string; duration: number }): void
|
||||
(e: 'emoji'): void
|
||||
(e: 'image'): void
|
||||
(e: 'file'): void
|
||||
(e: 'more'): void
|
||||
(e: 'mention', member: GroupMember): void
|
||||
(e: 'update:modelValue', val: string): void
|
||||
(e: 'send', val: string): void
|
||||
(e: 'send-voice', data: { path: string, duration: number }): void
|
||||
(e: 'send-image'): void
|
||||
(e: 'send-camera'): void
|
||||
(e: 'send-file'): void
|
||||
(e: 'focus'): void
|
||||
}>()
|
||||
|
||||
const { isDark } = useTheme()
|
||||
|
||||
// 文本输入相关
|
||||
const inputValue = ref(props.modelValue)
|
||||
const showEmoji = ref(false)
|
||||
const showMore = ref(false)
|
||||
const showMentionPicker = ref(false)
|
||||
|
||||
// 语音录制相关
|
||||
// 录音状态
|
||||
const isRecording = ref(false)
|
||||
const isCancelRecording = ref(false)
|
||||
const recordDuration = ref(0)
|
||||
let recordTimer: ReturnType<typeof setInterval> | null = null
|
||||
let recorderManager: UniApp.RecorderManager | null = null
|
||||
let startY = 0
|
||||
let voicePath = ''
|
||||
let recordTimer: any = null
|
||||
let recorderManager: any = null
|
||||
let recordStartY = 0
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
inputValue.value = val
|
||||
// 提及用户
|
||||
const mentionUsers = computed<MentionUser[]>(() => {
|
||||
return props.members.map(m => ({
|
||||
id: m.user_id,
|
||||
name: m.nickname || m.user?.name || '未知',
|
||||
avatar: m.user?.avatar
|
||||
}))
|
||||
})
|
||||
|
||||
watch(inputValue, (val) => {
|
||||
emit('update:modelValue', val)
|
||||
})
|
||||
// 同步
|
||||
watch(() => props.modelValue, (val) => inputValue.value = val)
|
||||
watch(inputValue, (val) => emit('update:modelValue', val))
|
||||
|
||||
// ========== 文本输入处理 (保持原有逻辑) ==========
|
||||
// 方法
|
||||
function onInput(e: any) {
|
||||
const value = e.detail?.value || e.value || inputValue.value
|
||||
// 检测 @ 符号
|
||||
if (props.isGroup && value.endsWith('@')) {
|
||||
const val = e.detail?.value || inputValue.value
|
||||
if (props.isGroup && val.endsWith('@')) {
|
||||
showMentionPicker.value = true
|
||||
}
|
||||
}
|
||||
|
||||
function openMentionPicker() {
|
||||
showMentionPicker.value = true
|
||||
}
|
||||
|
||||
function selectMention(member: GroupMember) {
|
||||
const name = member.nickname || member.user?.name || ''
|
||||
if (inputValue.value.endsWith('@')) {
|
||||
inputValue.value = inputValue.value + name + ' '
|
||||
} else {
|
||||
inputValue.value = inputValue.value + '@' + name + ' '
|
||||
}
|
||||
showMentionPicker.value = false
|
||||
emit('mention', member)
|
||||
}
|
||||
|
||||
function onSend() {
|
||||
if (!inputValue.value.trim()) return
|
||||
emit('send', inputValue.value.trim())
|
||||
inputValue.value = ''
|
||||
showEmoji.value = false
|
||||
}
|
||||
|
||||
// ========== 语音录制处理 ==========
|
||||
function initRecorderManager() {
|
||||
if (recorderManager) return recorderManager
|
||||
|
||||
function toggleEmoji() {
|
||||
showEmoji.value = !showEmoji.value
|
||||
showMore.value = false
|
||||
}
|
||||
|
||||
function handleEmojiSelect(emoji: string) {
|
||||
inputValue.value += emoji
|
||||
}
|
||||
|
||||
function handleMentionSelect(user: MentionUser) {
|
||||
inputValue.value = inputValue.value.endsWith('@')
|
||||
? inputValue.value + user.name + ' '
|
||||
: inputValue.value + '@' + user.name + ' '
|
||||
showMentionPicker.value = false
|
||||
}
|
||||
|
||||
function emitAction(action: 'send-image' | 'send-camera' | 'send-file') {
|
||||
showMore.value = false
|
||||
emit(action)
|
||||
}
|
||||
|
||||
// 录音逻辑 (核心逻辑保持不变,确保事件 emit 正确)
|
||||
function initRecorder() {
|
||||
recorderManager = uni.getRecorderManager()
|
||||
|
||||
recorderManager.onStart(() => {
|
||||
console.log('录音开始')
|
||||
isRecording.value = true
|
||||
recordDuration.value = 0
|
||||
recordTimer = setInterval(() => {
|
||||
recordDuration.value++
|
||||
// 最长60秒
|
||||
if (recordDuration.value >= 60) {
|
||||
stopRecording()
|
||||
}
|
||||
}, 1000)
|
||||
})
|
||||
|
||||
recorderManager.onStop((res) => {
|
||||
console.log('录音结束', res)
|
||||
clearRecordTimer()
|
||||
isRecording.value = false
|
||||
|
||||
if (!isCancelRecording.value && res.tempFilePath && recordDuration.value >= 1) {
|
||||
voicePath = res.tempFilePath
|
||||
emit('sendVoice', {
|
||||
path: voicePath,
|
||||
duration: recordDuration.value
|
||||
})
|
||||
}
|
||||
|
||||
isCancelRecording.value = false
|
||||
recordDuration.value = 0
|
||||
})
|
||||
|
||||
recorderManager.onError((err) => {
|
||||
console.error('录音错误', err)
|
||||
clearRecordTimer()
|
||||
isRecording.value = false
|
||||
isCancelRecording.value = false
|
||||
uni.showToast({ title: '录音失败', icon: 'none' })
|
||||
})
|
||||
|
||||
return recorderManager
|
||||
}
|
||||
|
||||
function clearRecordTimer() {
|
||||
if (recordTimer) {
|
||||
recorderManager.onStart(() => { isRecording.value = true; recordDuration.value = 0; recordTimer = setInterval(() => recordDuration.value++, 1000) })
|
||||
recorderManager.onStop((res: any) => {
|
||||
clearInterval(recordTimer)
|
||||
recordTimer = null
|
||||
}
|
||||
isRecording.value = false
|
||||
if (!isCancelRecording.value && res.tempFilePath) {
|
||||
emit('send-voice', { path: res.tempFilePath, duration: recordDuration.value })
|
||||
}
|
||||
isCancelRecording.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function startRecording(e: TouchEvent) {
|
||||
startY = e.touches[0].clientY
|
||||
isCancelRecording.value = false
|
||||
|
||||
// 初始化录音管理器
|
||||
const recorder = initRecorderManager()
|
||||
|
||||
// 开始录音
|
||||
recorder.start({
|
||||
duration: 60000, // 最长60秒
|
||||
sampleRate: 16000,
|
||||
numberOfChannels: 1,
|
||||
encodeBitRate: 48000,
|
||||
format: 'mp3'
|
||||
})
|
||||
recordStartY = e.touches[0].clientY
|
||||
if (!recorderManager) initRecorder()
|
||||
recorderManager.start({ format: 'aac' })
|
||||
}
|
||||
|
||||
function onRecordingMove(e: TouchEvent) {
|
||||
if (!isRecording.value) return
|
||||
|
||||
const currentY = e.touches[0].clientY
|
||||
const diff = startY - currentY
|
||||
|
||||
// 上滑超过80px触发取消
|
||||
isCancelRecording.value = diff > 80
|
||||
if (recordStartY - e.touches[0].clientY > 80) isCancelRecording.value = true
|
||||
}
|
||||
|
||||
function stopRecording() {
|
||||
if (!isRecording.value) return
|
||||
|
||||
if (recorderManager) {
|
||||
recorderManager.stop()
|
||||
}
|
||||
}
|
||||
|
||||
function cancelRecording() {
|
||||
isCancelRecording.value = true
|
||||
stopRecording()
|
||||
}
|
||||
|
||||
function formatRecordDuration(seconds: number): string {
|
||||
const m = Math.floor(seconds / 60).toString().padStart(2, '0')
|
||||
const s = (seconds % 60).toString().padStart(2, '0')
|
||||
return `${m}:${s}`
|
||||
}
|
||||
|
||||
// 暴露方法供父组件使用
|
||||
defineExpose({
|
||||
openMentionPicker
|
||||
})
|
||||
function stopRecording() { recorderManager?.stop() }
|
||||
function cancelRecording() { isCancelRecording.value = true; stopRecording() }
|
||||
function formatRecordDuration(s: number) { return `${Math.floor(s/60).toString().padStart(2,'0')}:${(s%60).toString().padStart(2,'0')}` }
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// ==========================================
|
||||
// 与设计稿 聊天页面.html 第116-123行 完全一致
|
||||
// ==========================================
|
||||
.message-input {
|
||||
// 亮色模式颜色 (与设计稿一致)
|
||||
--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: #f3f4f6; // 设计稿: border-gray-100
|
||||
--bg-content: #ffffff; // 设计稿: bg-white
|
||||
|
||||
position: relative;
|
||||
background: var(--bg-content);
|
||||
border-top: 2rpx solid var(--border-color);
|
||||
transition: all 0.3s;
|
||||
|
||||
// 暗色模式 - Warm Stone (与设计稿一致)
|
||||
/* =================================================================
|
||||
Message Input Styling - Warm Stone Restoration
|
||||
基于 聊天页面.html 的设计规范
|
||||
================================================================= */
|
||||
|
||||
.message-input-container {
|
||||
/* 颜色变量 - 严格对标 HTML */
|
||||
--bg-floater: #ffffff;
|
||||
--bg-input: #f3f4f6; /* gray-100 */
|
||||
--text-input: #1f2937; /* gray-800 */
|
||||
--text-placeholder: #9ca3af;/* gray-400 */
|
||||
--icon-normal: #6b7280; /* gray-500 */
|
||||
--icon-hover: #4f46e5; /* indigo-600 */
|
||||
--border-color: #f3f4f6; /* gray-100 */
|
||||
|
||||
&.theme-dark {
|
||||
--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: #292524; // 设计稿: dark:border-warm-800
|
||||
--bg-content: #1c1917; // 设计稿: dark:bg-warm-900
|
||||
--bg-floater: #1c1917; /* warm-900 */
|
||||
--bg-input: #292524; /* warm-800 */
|
||||
--text-input: #e7e5e4; /* warm-100 */
|
||||
--text-placeholder: #78716c;/* warm-500 */
|
||||
--icon-normal: #a8a29e; /* warm-400 */
|
||||
--icon-hover: #fb923c; /* orange-400 */
|
||||
--border-color: #292524; /* warm-800 */
|
||||
}
|
||||
}
|
||||
|
||||
// 底部输入栏 - 设计稿: px-4 py-3 pb-8 flex items-end gap-3
|
||||
.input-bar {
|
||||
display: flex;
|
||||
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
|
||||
/* 底部悬浮条容器 */
|
||||
.input-floater {
|
||||
position: fixed;
|
||||
bottom: 0; left: 0; right: 0;
|
||||
z-index: 200;
|
||||
background: var(--bg-floater);
|
||||
border-top: 2rpx solid var(--border-color);
|
||||
|
||||
/* 间距还原: px-4 py-3 pb-8 */
|
||||
padding: 24rpx 32rpx;
|
||||
padding-bottom: calc(64rpx + env(safe-area-inset-bottom));
|
||||
|
||||
transition: background-color 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
|
||||
// 语音按钮
|
||||
.voice-btn {
|
||||
/* 胶囊布局: Flex Row + Gap 3 (24rpx)
|
||||
修改点:align-items: center 实现垂直居中
|
||||
*/
|
||||
.input-bar-capsule {
|
||||
display: flex;
|
||||
align-items: center; /* 核心修改: 垂直居中对齐 */
|
||||
gap: 24rpx; /* gap-3 */
|
||||
}
|
||||
|
||||
/* 图标按钮: w-7 h-7 (56rpx) */
|
||||
.icon-btn {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--icon-normal);
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 16rpx;
|
||||
color: var(--icon-color);
|
||||
transition: color 0.15s;
|
||||
|
||||
margin-bottom: 0; /* 修改点: 移除底部 margin,因为已经是垂直居中了 */
|
||||
transition: color 0.2s, transform 0.1s;
|
||||
|
||||
svg {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
stroke-width: 2; /* 确保图标线条清晰 */
|
||||
}
|
||||
|
||||
|
||||
&:active {
|
||||
color: var(--icon-hover);
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
&.recording {
|
||||
color: var(--icon-hover);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
// 输入框容器 - 与设计稿完全一致
|
||||
// 设计稿: 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 {
|
||||
/* 输入框容器: 核心高度调整
|
||||
新设计: min-h-[36px] (72rpx) -> 精致
|
||||
*/
|
||||
.input-field-wrap {
|
||||
flex: 1;
|
||||
min-height: 88rpx; // 设计稿: min-h-[44px] = 44px = 88rpx
|
||||
background: var(--input-bg);
|
||||
border-radius: 32rpx; // 设计稿: rounded-2xl = 16px = 32rpx
|
||||
padding: 20rpx 32rpx; // 设计稿: px-4 py-2.5 = 16px 10px = 32rpx 20rpx
|
||||
min-height: 72rpx;
|
||||
background: var(--bg-input);
|
||||
border-radius: 36rpx; /* 圆角对应高度的一半 */
|
||||
padding: 16rpx 28rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 2rpx solid transparent; // 设计稿: border border-transparent
|
||||
transition: all 0.2s;
|
||||
|
||||
// 设计稿: dark:border-warm-700
|
||||
.theme-dark & {
|
||||
border-color: #44403c; // warm-700
|
||||
transition: background-color 0.3s;
|
||||
|
||||
.text-input {
|
||||
width: 100%;
|
||||
max-height: 200rpx;
|
||||
min-height: 40rpx;
|
||||
font-size: 30rpx; /* text-sm approx */
|
||||
line-height: 1.4;
|
||||
color: var(--text-input);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.input-placeholder {
|
||||
color: var(--text-placeholder);
|
||||
}
|
||||
}
|
||||
|
||||
// 设计稿: text-sm text-gray-800 dark:text-warm-100 resize-none h-5
|
||||
.text-input {
|
||||
width: 100%;
|
||||
min-height: 40rpx; // 设计稿: h-5 = 20px = 40rpx
|
||||
max-height: 200rpx;
|
||||
font-size: 28rpx; // 设计稿: text-sm = 14px = 28rpx
|
||||
line-height: 1.5;
|
||||
color: var(--input-text);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.input-placeholder {
|
||||
color: var(--placeholder-color);
|
||||
}
|
||||
|
||||
// 右侧操作按钮
|
||||
/* 右侧操作区 */
|
||||
.right-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 16rpx;
|
||||
gap: 24rpx; /* gap-3 between smile and plus */
|
||||
margin-bottom: 0; /* 修改点: 移除底部 margin */
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--icon-color);
|
||||
transition: all 0.15s;
|
||||
|
||||
svg {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: var(--icon-hover);
|
||||
}
|
||||
}
|
||||
|
||||
// 发送按钮
|
||||
/* 发送按钮 - 视觉焦点 */
|
||||
.send-btn {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 50%;
|
||||
background: var(--send-bg);
|
||||
/* 渐变色还原: Indigo / Orange */
|
||||
background: linear-gradient(135deg, #6366F1, #4F46E5);
|
||||
.theme-dark & { background: linear-gradient(135deg, #fb923c, #ea580c); }
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4rpx 16rpx rgba(79, 70, 229, 0.3);
|
||||
transition: all 0.2s;
|
||||
animation: popIn 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
|
||||
.theme-dark & {
|
||||
box-shadow: 0 4rpx 16rpx rgba(249, 115, 22, 0.3);
|
||||
}
|
||||
|
||||
margin-bottom: 0; /* 修改点: 移除底部 margin */
|
||||
box-shadow: 0 4rpx 12rpx rgba(79, 70, 229, 0.3);
|
||||
|
||||
svg {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
}
|
||||
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes popIn {
|
||||
from { transform: scale(0); }
|
||||
to { transform: scale(1); }
|
||||
}
|
||||
|
||||
// 录音浮层
|
||||
/* 录音浮层 (复用) */
|
||||
.recording-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
animation: fadeIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.recording-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 60rpx;
|
||||
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;
|
||||
height: 8rpx;
|
||||
}
|
||||
position: fixed; inset: 0; background: rgba(0,0,0,0.6); z-index: 999;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
.recording-content {
|
||||
background: var(--bg-floater); padding: 64rpx; border-radius: 48rpx;
|
||||
display: flex; flex-direction: column; align-items: center;
|
||||
&.cancel-mode { .recording-icon { background: #ef4444; } }
|
||||
}
|
||||
.recording-icon { width: 128rpx; height: 128rpx; background: #10b981; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: #fff; margin-bottom: 32rpx; svg { width: 64rpx; height: 64rpx; } }
|
||||
.wave-bars { display: flex; gap: 8rpx; height: 48rpx; margin-bottom: 24rpx; .wave-bar { width: 8rpx; height: 16rpx; background: #10b981; border-radius: 8rpx; animation: wave 1s infinite; } }
|
||||
.recording-duration { font-size: 36rpx; font-weight: 700; color: var(--text-input); margin-bottom: 16rpx; }
|
||||
.recording-tip { font-size: 24rpx; color: var(--icon-normal); }
|
||||
}
|
||||
@keyframes wave { 0%, 100% { height: 16rpx; } 50% { height: 48rpx; } }
|
||||
|
||||
.recording-icon {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 50%;
|
||||
background: #10b981;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 32rpx;
|
||||
transition: background 0.2s;
|
||||
|
||||
svg {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.wave-bars {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8rpx;
|
||||
height: 64rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.wave-bar {
|
||||
width: 8rpx;
|
||||
height: 16rpx;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes wave {
|
||||
0%, 100% { height: 16rpx; }
|
||||
50% { height: 48rpx; }
|
||||
}
|
||||
|
||||
.recording-duration {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: var(--input-text);
|
||||
font-family: monospace;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.recording-tip {
|
||||
font-size: 24rpx;
|
||||
color: var(--placeholder-color);
|
||||
}
|
||||
|
||||
// @ 提及选择器 (保持原有样式)
|
||||
.mention-picker {
|
||||
max-height: 60vh;
|
||||
background: var(--bg-content);
|
||||
|
||||
.mention-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx 30rpx;
|
||||
border-bottom: 1rpx solid var(--border-color);
|
||||
|
||||
.title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: var(--input-text);
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--icon-color);
|
||||
|
||||
svg {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mention-list {
|
||||
max-height: 50vh;
|
||||
}
|
||||
|
||||
.mention-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx 30rpx;
|
||||
gap: 20rpx;
|
||||
|
||||
&:active {
|
||||
background: var(--input-bg);
|
||||
}
|
||||
|
||||
.mention-name {
|
||||
font-size: 30rpx;
|
||||
color: var(--input-text);
|
||||
/* 更多面板 Grid (复用) */
|
||||
.tools-grid {
|
||||
padding: 48rpx 32rpx; display: grid; grid-template-columns: repeat(4, 1fr); gap: 32rpx;
|
||||
.tool-item {
|
||||
display: flex; flex-direction: column; align-items: center; gap: 16rpx;
|
||||
.tool-icon {
|
||||
width: 112rpx; height: 112rpx; border-radius: 32rpx; display: flex; align-items: center; justify-content: center;
|
||||
&.album { background: #10b981; } &.camera { background: #f97316; } &.file { background: #eab308; }
|
||||
svg { width: 56rpx; height: 56rpx; }
|
||||
}
|
||||
text { font-size: 24rpx; color: var(--icon-normal); }
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,123 +1,102 @@
|
||||
<template>
|
||||
<view class="detail-container" :class="{ 'theme-dark': isDark }">
|
||||
<!-- 头部背景区 (视差效果) -->
|
||||
<view class="profile-header">
|
||||
<view class="blur-bg" :style="headerBgStyle" />
|
||||
<view class="gradient-mask" />
|
||||
<!-- 视差背景 (与设计稿一致: blur-xl scale-110 opacity-70) -->
|
||||
<view class="parallax-bg">
|
||||
<image
|
||||
v-if="contact?.user?.avatar"
|
||||
:src="contact.user.avatar"
|
||||
class="blur-image"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="gradient-overlay" />
|
||||
</view>
|
||||
|
||||
<!-- 返回按钮 -->
|
||||
<!-- 导航栏 (与设计稿一致) -->
|
||||
<view class="nav-header">
|
||||
<view class="nav-back" @click="goBack">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M15 18l-6-6 6-6"/>
|
||||
</svg>
|
||||
</view>
|
||||
<view class="nav-more">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="1"/>
|
||||
<circle cx="19" cy="12" r="1"/>
|
||||
<circle cx="5" cy="12" r="1"/>
|
||||
</svg>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 主体内容 (浮动卡片) -->
|
||||
<scroll-view scroll-y class="main-content custom-scrollbar" v-if="!loading && contact">
|
||||
<view class="content-wrapper">
|
||||
|
||||
<!-- 用户身份卡片 -->
|
||||
<view class="identity-card animate-fade-in-up">
|
||||
<view class="avatar-wrapper">
|
||||
<app-avatar
|
||||
:src="contact.user?.avatar"
|
||||
:name="contact.remark_name || contact.user?.name"
|
||||
:size="256"
|
||||
round
|
||||
custom-style="border: 12rpx solid var(--bg-card);"
|
||||
/>
|
||||
</view>
|
||||
<view class="info-block">
|
||||
<text class="user-name">{{ contact.remark_name || contact.user?.name || '未知' }}</text>
|
||||
<view class="meta-row">
|
||||
<view class="meta-tag">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="12" cy="7" r="4"/>
|
||||
</svg>
|
||||
<text>ID: {{ contact.user?.id || '-' }}</text>
|
||||
</view>
|
||||
<view class="meta-tag">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/>
|
||||
<circle cx="12" cy="10" r="3"/>
|
||||
</svg>
|
||||
<text>{{ contact.user?.region || '未知地区' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="bio-text">{{ contact.user?.desc || '这个人很懒,什么都没写' }}</text>
|
||||
</view>
|
||||
<!-- 主体内容 -->
|
||||
<scroll-view scroll-y class="main-scroll custom-scrollbar" v-if="!loading && contact">
|
||||
<!-- 身份卡片区 (与设计稿一致) -->
|
||||
<view class="identity-section">
|
||||
<!-- 头像: w-32 h-32 rounded-full border-[6px] border-white shadow-xl -->
|
||||
<view class="avatar-wrapper">
|
||||
<app-avatar
|
||||
:src="contact.user?.avatar"
|
||||
:name="contact.remark_name || contact.user?.name"
|
||||
:size="256"
|
||||
round
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 设置组:资料 -->
|
||||
<view class="settings-group animate-fade-in-up" style="animation-delay: 100ms;">
|
||||
<view class="settings-card">
|
||||
<view class="setting-item" @click="editRemark">
|
||||
<view class="icon-wrap color-blue">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2">
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
||||
</svg>
|
||||
</view>
|
||||
<view class="item-body">
|
||||
<text class="item-title">设置备注</text>
|
||||
<text class="item-value">{{ contact.remark_name || '未设置' }}</text>
|
||||
</view>
|
||||
<svg class="chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="9 18 15 12 9 6"/>
|
||||
</svg>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 名字: text-2xl font-bold -->
|
||||
<text class="user-name">{{ contact.remark_name || contact.user?.name || '未知' }}</text>
|
||||
|
||||
<!-- 标签: text-xs bg-gray-100 px-2 py-0.5 rounded -->
|
||||
<view class="tag-row">
|
||||
<view class="info-tag">ID: {{ contact.user?.id || '-' }}</view>
|
||||
<view class="info-tag">{{ contact.user?.region || '未知' }}</view>
|
||||
</view>
|
||||
|
||||
<!-- 设置组:交互 -->
|
||||
<view class="settings-group animate-fade-in-up" style="animation-delay: 150ms;">
|
||||
<view class="group-label">隐私与通知</view>
|
||||
<view class="settings-card">
|
||||
<view class="setting-item">
|
||||
<view class="icon-wrap color-orange">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2">
|
||||
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>
|
||||
</svg>
|
||||
</view>
|
||||
<view class="item-body">
|
||||
<text class="item-title">置顶聊天</text>
|
||||
</view>
|
||||
<wd-switch v-model="contact.is_top" size="24px" :active-color="isDark ? '#f97316' : '#4F46E5'" @change="toggleTop" />
|
||||
</view>
|
||||
|
||||
<view class="setting-item">
|
||||
<view class="icon-wrap color-purple">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2">
|
||||
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/>
|
||||
<path d="M13.73 21a2 2 0 0 1-3.46 0"/>
|
||||
<line x1="1" y1="1" x2="23" y2="23"/>
|
||||
</svg>
|
||||
</view>
|
||||
<view class="item-body">
|
||||
<text class="item-title">消息免打扰</text>
|
||||
</view>
|
||||
<wd-switch v-model="contact.is_muted" size="24px" :active-color="isDark ? '#f97316' : '#4F46E5'" @change="toggleMute" />
|
||||
</view>
|
||||
|
||||
<view class="setting-item">
|
||||
<view class="icon-wrap color-red">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="4.93" y1="4.93" x2="19.07" y2="19.07"/>
|
||||
</svg>
|
||||
</view>
|
||||
<view class="item-body">
|
||||
<text class="item-title">加入黑名单</text>
|
||||
</view>
|
||||
<wd-switch v-model="contact.is_blocked" size="24px" active-color="#ef4444" @change="toggleBlock" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="spacer" />
|
||||
|
||||
<!-- 签名: text-sm text-gray-500 text-center max-w-[200px] -->
|
||||
<text class="bio-text">{{ contact.user?.desc || '这个人很懒,什么都没写' }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 好友设置卡片 (与设计稿一致) -->
|
||||
<view class="settings-card animate-fade-in-up">
|
||||
<!-- 设置备注 -->
|
||||
<view class="setting-row" @click="editRemark">
|
||||
<text class="row-label">设置备注</text>
|
||||
<view class="row-right">
|
||||
<text class="row-value">{{ contact.remark_name || '未设置' }}</text>
|
||||
<svg class="chevron-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="9 18 15 12 9 6"/>
|
||||
</svg>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 置顶聊天 -->
|
||||
<view class="setting-row">
|
||||
<text class="row-label">置顶聊天</text>
|
||||
<view class="toggle-wrap">
|
||||
<view
|
||||
class="custom-toggle"
|
||||
:class="{ active: contact.is_top }"
|
||||
@click="toggleTopManual"
|
||||
>
|
||||
<view class="toggle-thumb" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 消息免打扰 -->
|
||||
<view class="setting-row no-border">
|
||||
<text class="row-label">消息免打扰</text>
|
||||
<view class="toggle-wrap">
|
||||
<view
|
||||
class="custom-toggle"
|
||||
:class="{ active: contact.is_muted }"
|
||||
@click="toggleMuteManual"
|
||||
>
|
||||
<view class="toggle-thumb" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bottom-spacer" />
|
||||
</scroll-view>
|
||||
|
||||
<!-- Loading -->
|
||||
@@ -125,20 +104,23 @@
|
||||
<wd-loading size="60rpx" :color="isDark ? '#f97316' : '#4F46E5'"/>
|
||||
</view>
|
||||
|
||||
<!-- 底部悬浮操作栏 -->
|
||||
<!-- 底部悬浮操作栏 (与设计稿一致: rounded-t-[32px] shadow-[0_-10px_40px]) -->
|
||||
<view class="floating-dock" v-if="contact">
|
||||
<view class="dock-content">
|
||||
<view class="dock-buttons">
|
||||
<!-- 电话: flex-1 -->
|
||||
<view class="dock-btn secondary" @click="startAudioCall">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72"/>
|
||||
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.05 12.05 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.05 12.05 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/>
|
||||
</svg>
|
||||
</view>
|
||||
<!-- 发消息: flex-[2] -->
|
||||
<view class="dock-btn primary" @click="goChat">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
|
||||
</svg>
|
||||
<text>发消息</text>
|
||||
</view>
|
||||
<!-- 视频: flex-1 -->
|
||||
<view class="dock-btn secondary" @click="startVideoCall">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polygon points="23 7 16 12 23 17 23 7"/>
|
||||
@@ -146,8 +128,9 @@
|
||||
</svg>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="danger-btn" @click="deleteFriend">
|
||||
|
||||
<!-- 删除好友: text-xs text-gray-400 underline -->
|
||||
<view class="delete-link" @click="deleteFriend">
|
||||
<text>删除好友</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -158,7 +141,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import * as contactApi from '@/api/modules/contact'
|
||||
import { useToast, useMessage } from 'wot-design-uni'
|
||||
@@ -170,11 +153,6 @@ import type { Contact } from '@/types/api'
|
||||
const toast = useToast(); const messageBox = useMessage(); const { isDark } = useTheme();
|
||||
const loading = ref(true); const contact = ref<Contact | null>(null); const contactId = ref(''); const userId = ref('');
|
||||
|
||||
const headerBgStyle = computed(() => {
|
||||
const url = contact.value?.user?.avatar || '';
|
||||
return url ? `background-image: url(${url});` : 'background: var(--bg-card);';
|
||||
})
|
||||
|
||||
onLoad((options: any) => { contactId.value = options?.id || ''; userId.value = options?.userId || '' })
|
||||
onMounted(async () => { if (contactId.value) { await loadContact() } else if (userId.value) { await loadContactByUserId() } })
|
||||
async function loadContact() { loading.value = true; try { contact.value = await contactApi.getContactDetail(contactId.value) } catch (error) { console.error('加载联系人失败:', error); toast.error('加载失败') } finally { loading.value = false } }
|
||||
@@ -187,50 +165,51 @@ async function editRemark() { if (!contact.value) return; try { const { value }
|
||||
async function toggleTop() { if (!contact.value) return; try { await contactApi.updateContact(contactId.value, { is_top: contact.value.is_top }); toast.success(contact.value.is_top ? '已置顶' : '已取消置顶') } catch { contact.value.is_top = !contact.value.is_top; toast.error('操作失败') } }
|
||||
async function toggleMute() { if (!contact.value) return; try { await contactApi.updateContact(contactId.value, { is_muted: contact.value.is_muted }); toast.success(contact.value.is_muted ? '已开启免打扰' : '已关闭免打扰') } catch { contact.value.is_muted = !contact.value.is_muted; toast.error('操作失败') } }
|
||||
async function toggleBlock() { if (!contact.value) return; try { await contactApi.updateContact(contactId.value, { is_blocked: contact.value.is_blocked }); toast.success(contact.value.is_blocked ? '已加入黑名单' : '已移出黑名单') } catch { contact.value.is_blocked = !contact.value.is_blocked; toast.error('操作失败') } }
|
||||
// 手动切换方法 (用于自定义开关)
|
||||
async function toggleTopManual() { if (!contact.value) return; contact.value.is_top = !contact.value.is_top; await toggleTop() }
|
||||
async function toggleMuteManual() { if (!contact.value) return; contact.value.is_muted = !contact.value.is_muted; await toggleMute() }
|
||||
async function deleteFriend() { try { await messageBox.confirm({ title: '提示', msg: '确定要删除该好友吗?删除后将无法恢复' }); await contactApi.deleteContact(contactId.value); toast.success('删除成功'); setTimeout(() => { uni.navigateBack() }, 1000) } catch (e: any) { if (e !== 'cancel') { toast.error('删除失败') } } }
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// ==========================================
|
||||
// 页面容器 - 浅色模式 (与设计稿完全一致)
|
||||
// ==========================================
|
||||
.detail-container {
|
||||
--bg-base: #ffffff; // 白色页面背景
|
||||
--bg-page: #f3f4f6; // gray-100 (页面背景)
|
||||
--bg-card: #ffffff; // 卡片背景
|
||||
--bg-secondary: #f3f4f6; // gray-100 (次要背景/按钮)
|
||||
--text-main: #1f2937; // gray-900 (名称)
|
||||
--text-sub: #6b7280; // gray-500 (签名/标签)
|
||||
--text-tertiary: #9ca3af; // gray-400 (值)
|
||||
--bg-tag: #f3f4f6; // gray-100 (标签背景)
|
||||
--bg-toggle: #e5e7eb; // gray-200 (开关关闭)
|
||||
--text-main: #111827; // gray-900 (名称)
|
||||
--text-sub: #6b7280; // gray-500 (签名)
|
||||
--text-tertiary: #9ca3af; // gray-400 (标签/值)
|
||||
--color-brand: #4F46E5; // indigo-600
|
||||
--border-color: #f9fafb; // gray-50
|
||||
--gradient-to: #ffffff; // 渐变终点色
|
||||
|
||||
min-height: 100vh;
|
||||
background: var(--bg-base);
|
||||
background: var(--bg-page);
|
||||
position: relative;
|
||||
|
||||
// 深色模式 - Warm Stone (与设计稿完全一致)
|
||||
&.theme-dark {
|
||||
--bg-base: #0c0a09; // warm-950 (极深背景)
|
||||
--bg-page: #0c0a09; // warm-950
|
||||
--bg-card: #1c1917; // warm-900
|
||||
--bg-secondary: #292524; // warm-800
|
||||
--text-main: #f5f5f4; // warm-100
|
||||
--bg-tag: #292524; // warm-800
|
||||
--bg-toggle: #44403c; // warm-700
|
||||
--text-main: #fafaf9; // warm-50
|
||||
--text-sub: #a8a29e; // warm-400
|
||||
--text-tertiary: #78716c; // warm-500
|
||||
--color-brand: #f97316; // orange-500
|
||||
--border-color: rgba(41, 37, 36, 0.5); // warm-800/50
|
||||
--border-color: rgba(41, 37, 36, 0.5);
|
||||
--gradient-to: #0c0a09; // warm-950
|
||||
}
|
||||
}
|
||||
|
||||
// 动画
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20rpx);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
from { opacity: 0; transform: translateY(20rpx); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.animate-fade-in-up {
|
||||
@@ -244,304 +223,350 @@ async function deleteFriend() { try { await messageBox.confirm({ title: '提示'
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
// 视差头部背景
|
||||
.profile-header {
|
||||
// ==========================================
|
||||
// 视差背景 (与设计稿一致: blur-xl scale-110 opacity-70)
|
||||
// ==========================================
|
||||
.parallax-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 600rpx;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
z-index: 0;
|
||||
|
||||
.blur-bg {
|
||||
.blur-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
filter: blur(40px);
|
||||
transform: scale(1.1);
|
||||
opacity: 0.7;
|
||||
// blur-xl = 24px, 需要用px单位
|
||||
filter: blur(24px);
|
||||
-webkit-filter: blur(24px);
|
||||
transform: scale(1.1); // scale-110
|
||||
opacity: 0.7; // opacity-70
|
||||
|
||||
.theme-dark & {
|
||||
opacity: 0.4;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.gradient-mask {
|
||||
.gradient-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(to bottom, transparent 0%, var(--gradient-to) 85%);
|
||||
}
|
||||
|
||||
.nav-back {
|
||||
position: absolute;
|
||||
top: calc(var(--status-bar-height) + 16rpx);
|
||||
left: 24rpx;
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
background: rgba(0, 0, 0, 0.15);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
backdrop-filter: blur(10px);
|
||||
z-index: 10;
|
||||
transition: all 0.15s;
|
||||
|
||||
svg {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
background: linear-gradient(to bottom, transparent 0%, var(--gradient-to) 100%);
|
||||
}
|
||||
}
|
||||
|
||||
// 主内容
|
||||
.main-content {
|
||||
height: 100vh;
|
||||
// ==========================================
|
||||
// 导航栏 (与设计稿一致)
|
||||
// ==========================================
|
||||
.nav-header {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
padding: calc(var(--status-bar-height) + 24rpx) 32rpx 16rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.nav-back, .nav-more {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: opacity 0.15s;
|
||||
|
||||
svg {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
color: var(--text-main);
|
||||
|
||||
.theme-dark & {
|
||||
color: #e7e5e4; // warm-200
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-more svg {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 主体滚动区
|
||||
// ==========================================
|
||||
.main-scroll {
|
||||
height: calc(100vh - 120rpx);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
padding: 0 32rpx;
|
||||
padding-top: calc(220rpx + var(--status-bar-height));
|
||||
}
|
||||
|
||||
// 身份卡片
|
||||
.identity-card {
|
||||
// ==========================================
|
||||
// 身份卡片区 (与设计稿完全一致)
|
||||
// ==========================================
|
||||
.identity-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 60rpx;
|
||||
padding: 64rpx 48rpx 64rpx;
|
||||
|
||||
// 头像: w-32 h-32 rounded-full border-[6px] border-white shadow-xl
|
||||
.avatar-wrapper {
|
||||
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.15);
|
||||
border-radius: 50%;
|
||||
border: 12rpx solid var(--bg-card); // border-[6px] = 12rpx
|
||||
box-shadow: 0 20rpx 50rpx rgba(0, 0, 0, 0.2); // shadow-xl
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.info-block {
|
||||
margin-top: 30rpx;
|
||||
text-align: center;
|
||||
|
||||
.user-name {
|
||||
font-size: 48rpx;
|
||||
font-weight: 700;
|
||||
color: var(--text-main);
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.meta-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.meta-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: var(--text-sub);
|
||||
background: var(--bg-secondary);
|
||||
padding: 10rpx 20rpx;
|
||||
border-radius: 20rpx;
|
||||
|
||||
svg {
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bio-text {
|
||||
font-size: 28rpx;
|
||||
color: var(--text-sub);
|
||||
max-width: 500rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 设置组
|
||||
.settings-group {
|
||||
margin-bottom: 40rpx;
|
||||
|
||||
.group-label {
|
||||
font-size: 26rpx;
|
||||
color: var(--text-sub);
|
||||
// 名字: text-2xl font-bold
|
||||
.user-name {
|
||||
font-size: 48rpx; // text-2xl = 24px = 48rpx
|
||||
font-weight: 700; // font-bold
|
||||
color: var(--text-main);
|
||||
margin-bottom: 16rpx;
|
||||
margin-left: 12rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
// 标签行
|
||||
.tag-row {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
// 标签: text-xs bg-gray-100 px-2 py-0.5 rounded
|
||||
.info-tag {
|
||||
font-size: 24rpx; // text-xs = 12px = 24rpx
|
||||
color: var(--text-tertiary);
|
||||
background: var(--bg-tag);
|
||||
padding: 8rpx 16rpx; // px-2 py-0.5
|
||||
border-radius: 8rpx; // rounded
|
||||
}
|
||||
|
||||
// 签名: text-sm text-gray-500 text-center max-w-[200px]
|
||||
.bio-text {
|
||||
font-size: 28rpx; // text-sm = 14px = 28rpx
|
||||
color: var(--text-sub);
|
||||
text-align: center;
|
||||
line-height: 1.6; // leading-relaxed
|
||||
max-width: 400rpx; // max-w-[200px] = 400rpx
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 设置卡片 (与设计稿一致: rounded-2xl shadow-sm border)
|
||||
// ==========================================
|
||||
.settings-card {
|
||||
margin: 0 24rpx 48rpx;
|
||||
background: var(--bg-card);
|
||||
border-radius: 24rpx;
|
||||
border-radius: 32rpx; // rounded-2xl = 16px = 32rpx
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04); // shadow-sm
|
||||
border: 2rpx solid var(--border-color);
|
||||
overflow: hidden;
|
||||
|
||||
.theme-dark & {
|
||||
box-shadow: none;
|
||||
border-color: rgba(41, 37, 36, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
// 设置行 (与设计稿一致)
|
||||
.setting-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 28rpx 28rpx;
|
||||
border-bottom: 1rpx solid rgba(0, 0, 0, 0.04);
|
||||
justify-content: space-between;
|
||||
padding: 32rpx; // p-4 = 16px = 32rpx
|
||||
border-bottom: 2rpx solid var(--border-color);
|
||||
transition: background 0.15s;
|
||||
|
||||
.theme-dark & {
|
||||
border-bottom-color: rgba(255, 255, 255, 0.06);
|
||||
border-bottom-color: rgba(41, 37, 36, 0.5);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
&.no-border {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
|
||||
.theme-dark & {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
}
|
||||
|
||||
.icon-wrap {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 16rpx;
|
||||
.row-label {
|
||||
font-size: 28rpx; // text-sm = 14px = 28rpx
|
||||
font-weight: 500; // font-medium
|
||||
color: var(--text-main);
|
||||
|
||||
.theme-dark & {
|
||||
color: #e7e5e4; // warm-200
|
||||
}
|
||||
}
|
||||
|
||||
.row-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 24rpx;
|
||||
|
||||
svg {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
}
|
||||
|
||||
&.color-blue { background: #0ea5e9; }
|
||||
&.color-orange { background: #f97316; }
|
||||
&.color-purple { background: #8b5cf6; }
|
||||
&.color-red { background: #ef4444; }
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.item-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.row-value {
|
||||
font-size: 24rpx; // text-xs
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.item-title {
|
||||
font-size: 30rpx;
|
||||
color: var(--text-main);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.item-value {
|
||||
font-size: 26rpx;
|
||||
color: var(--text-sub);
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.chevron {
|
||||
.chevron-icon {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
color: var(--text-sub);
|
||||
opacity: 0.5;
|
||||
color: #d1d5db; // gray-300
|
||||
}
|
||||
}
|
||||
|
||||
.spacer {
|
||||
height: 320rpx;
|
||||
// 自定义开关 (与设计稿一致)
|
||||
.toggle-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
// 底部操作台
|
||||
.custom-toggle {
|
||||
width: 80rpx; // w-10 = 40px = 80rpx
|
||||
height: 48rpx; // h-6 = 24px = 48rpx
|
||||
background: var(--bg-toggle);
|
||||
border-radius: 48rpx; // rounded-full
|
||||
position: relative;
|
||||
transition: background 0.2s;
|
||||
|
||||
.toggle-thumb {
|
||||
position: absolute;
|
||||
left: 8rpx;
|
||||
top: 8rpx;
|
||||
width: 32rpx; // w-4 = 16px = 32rpx
|
||||
height: 32rpx; // h-4 = 16px = 32rpx
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1); // shadow-sm
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--color-brand);
|
||||
|
||||
.toggle-thumb {
|
||||
transform: translateX(32rpx); // 移动到右边
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 底部悬浮操作栏 (与设计稿一致: rounded-t-[32px] shadow-[0_-10px_40px])
|
||||
// ==========================================
|
||||
.floating-dock {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 30rpx 40rpx;
|
||||
padding-bottom: calc(30rpx + env(safe-area-inset-bottom));
|
||||
padding: 32rpx 48rpx; // px-6 py-4
|
||||
padding-bottom: calc(64rpx + env(safe-area-inset-bottom)); // pb-8
|
||||
background: var(--bg-card);
|
||||
border-top: 1rpx solid rgba(0, 0, 0, 0.05);
|
||||
border-radius: 64rpx 64rpx 0 0;
|
||||
box-shadow: 0 -20rpx 80rpx rgba(0, 0, 0, 0.05);
|
||||
border-top: 2rpx solid var(--border-color);
|
||||
border-radius: 64rpx 64rpx 0 0; // rounded-t-[32px] = 64rpx
|
||||
box-shadow: 0 -20rpx 80rpx rgba(0, 0, 0, 0.05); // shadow-[0_-10px_40px]
|
||||
z-index: 100;
|
||||
|
||||
.theme-dark & {
|
||||
border-top-color: rgba(255, 255, 255, 0.05);
|
||||
border-top-color: #292524; // warm-800
|
||||
}
|
||||
}
|
||||
|
||||
// 按钮行
|
||||
.dock-buttons {
|
||||
display: flex;
|
||||
gap: 32rpx; // gap-4 = 16px = 32rpx
|
||||
margin-bottom: 32rpx; // mb-4
|
||||
}
|
||||
|
||||
.dock-btn {
|
||||
height: 96rpx; // h-12 = 48px = 96rpx
|
||||
border-radius: 32rpx; // rounded-2xl
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16rpx;
|
||||
transition: all 0.15s;
|
||||
|
||||
svg {
|
||||
width: 40rpx; // w-5 h-5 = 20px = 40rpx
|
||||
height: 40rpx;
|
||||
}
|
||||
|
||||
.dock-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.dock-btn {
|
||||
height: 100rpx;
|
||||
border-radius: 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.15s;
|
||||
// 次要按钮: flex-1
|
||||
&.secondary {
|
||||
flex: 1;
|
||||
background: var(--bg-tag);
|
||||
color: var(--text-main);
|
||||
|
||||
svg {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
.theme-dark & {
|
||||
background: #292524; // warm-800
|
||||
color: #e7e5e4; // warm-200
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
&.primary {
|
||||
flex: 2;
|
||||
background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%);
|
||||
color: #fff;
|
||||
gap: 16rpx;
|
||||
font-weight: 600;
|
||||
font-size: 32rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(79, 70, 229, 0.25);
|
||||
|
||||
.theme-dark & {
|
||||
background: linear-gradient(135deg, #fb923c 0%, #f97316 100%);
|
||||
box-shadow: 0 8rpx 24rpx rgba(249, 115, 22, 0.25);
|
||||
}
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
flex: 1;
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-main);
|
||||
&:hover {
|
||||
background: #e5e7eb; // gray-200
|
||||
.theme-dark & {
|
||||
background: #44403c; // warm-700
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.danger-btn {
|
||||
text-align: center;
|
||||
// 主要按钮: flex-[2]
|
||||
&.primary {
|
||||
flex: 2;
|
||||
background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%);
|
||||
color: #fff;
|
||||
font-weight: 700; // font-bold
|
||||
font-size: 28rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(99, 102, 241, 0.3);
|
||||
|
||||
text {
|
||||
color: var(--text-sub);
|
||||
font-size: 26rpx;
|
||||
padding: 10rpx 20rpx;
|
||||
opacity: 0.6;
|
||||
svg {
|
||||
stroke: #fff;
|
||||
}
|
||||
|
||||
&:active text {
|
||||
opacity: 1;
|
||||
color: #ef4444;
|
||||
.theme-dark & {
|
||||
background: linear-gradient(135deg, #fb923c 0%, #ea580c 100%);
|
||||
box-shadow: 0 8rpx 24rpx rgba(234, 88, 12, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除好友链接: text-xs text-gray-400 underline
|
||||
.delete-link {
|
||||
text-align: center;
|
||||
|
||||
text {
|
||||
font-size: 24rpx; // text-xs
|
||||
color: var(--text-tertiary);
|
||||
text-decoration: underline; // underline
|
||||
}
|
||||
|
||||
&:active text {
|
||||
color: #ef4444; // hover:text-red-500
|
||||
}
|
||||
}
|
||||
|
||||
// 底部留白
|
||||
.bottom-spacer {
|
||||
height: 360rpx;
|
||||
}
|
||||
|
||||
// 加载中
|
||||
.center-loading {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
@@ -549,3 +574,4 @@ async function deleteFriend() { try { await messageBox.confirm({ title: '提示'
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -32,22 +32,6 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 2. 搜索栏 (扁平化设计) -->
|
||||
<view class="search-wrapper">
|
||||
<view class="search-bar" @click="goSearch">
|
||||
<svg class="search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
</svg>
|
||||
<input
|
||||
type="text"
|
||||
class="search-input"
|
||||
placeholder="搜索"
|
||||
disabled
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 3. 会话列表 -->
|
||||
<scroll-view
|
||||
class="conversation-list custom-scrollbar"
|
||||
@@ -56,6 +40,22 @@
|
||||
:refresher-triggered="refreshing"
|
||||
@refresherrefresh="onRefresh"
|
||||
>
|
||||
|
||||
<!-- 2. 搜索栏 (扁平化设计) -->
|
||||
<view class="search-wrapper">
|
||||
<view class="search-bar" @click="goSearch">
|
||||
<svg class="search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
</svg>
|
||||
<input
|
||||
type="text"
|
||||
class="search-input"
|
||||
placeholder="搜索"
|
||||
disabled
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="loading && conversations.length === 0" class="loading-state">
|
||||
<wd-loading size="50rpx" color="var(--color-primary)" />
|
||||
<text>连接中...</text>
|
||||
|
||||
Reference in New Issue
Block a user