UI设计优化
This commit is contained in:
@@ -1,29 +1,80 @@
|
|||||||
<template>
|
<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
|
<app-avatar
|
||||||
:src="avatarUrl"
|
:src="avatar"
|
||||||
:name="avatarName"
|
:name="avatarName"
|
||||||
:size="72"
|
:size="80"
|
||||||
radius="8rpx"
|
radius="24rpx"
|
||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 消息内容 -->
|
<!-- 内容列 -->
|
||||||
<view class="bubble-content" :class="bubbleClass">
|
<view class="content-col" :class="{ 'items-end': isSelf }">
|
||||||
<slot />
|
<!-- 群聊昵称 -->
|
||||||
</view>
|
<text v-if="senderName" class="sender-name">{{ senderName }}</text>
|
||||||
|
|
||||||
<!-- 自己的头像 -->
|
<!-- 气泡实体 -->
|
||||||
<view v-if="isSelf" class="bubble-avatar">
|
<view
|
||||||
<app-avatar
|
class="bubble"
|
||||||
:src="selfAvatarUrl"
|
:class="[
|
||||||
:name="selfAvatarName"
|
isSelf ? 'bubble-self' : 'bubble-other',
|
||||||
:size="72"
|
mediaTypeClass
|
||||||
radius="8rpx"
|
]"
|
||||||
bg-color="#10aeff"
|
@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>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
@@ -31,90 +82,147 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import AppAvatar from '@/components/common/AppAvatar.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 {
|
const props = defineProps<{
|
||||||
isSelf?: boolean
|
msg: ChatMessage
|
||||||
avatarUrl?: string
|
isSelf: boolean
|
||||||
avatarName?: string
|
senderName?: string
|
||||||
selfAvatarUrl?: string
|
avatar?: string
|
||||||
selfAvatarName?: string
|
playingAudioId?: number | null
|
||||||
type?: 'text' | 'image' | 'audio' | 'video' | 'file'
|
}>()
|
||||||
|
|
||||||
|
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>(), {
|
function getFileName(msg: ChatMessage) {
|
||||||
isSelf: false,
|
const extra = typeof msg.extra === 'string' ? JSON.parse(msg.extra || '{}') : msg.extra
|
||||||
avatarUrl: '',
|
return extra?.name || '未知文件'
|
||||||
avatarName: '',
|
}
|
||||||
selfAvatarUrl: '',
|
|
||||||
selfAvatarName: '',
|
|
||||||
type: 'text'
|
|
||||||
})
|
|
||||||
|
|
||||||
const bubbleClass = computed(() => {
|
function getFileSize(msg: ChatMessage) {
|
||||||
const classes = []
|
const extra = typeof msg.extra === 'string' ? JSON.parse(msg.extra || '{}') : msg.extra
|
||||||
if (props.isSelf) {
|
return extra?.size || 0
|
||||||
classes.push('bubble-self')
|
}
|
||||||
} else {
|
|
||||||
classes.push('bubble-other')
|
function getVideoThumb(msg: ChatMessage) {
|
||||||
}
|
const extra = typeof msg.extra === 'string' ? JSON.parse(msg.extra || '{}') : msg.extra
|
||||||
if (props.type !== 'text') {
|
if (extra?.thumb || extra?.cover) return resolveImageUrl(extra.thumb || extra.cover)
|
||||||
classes.push('bubble-media')
|
return getMediaUrl(msg)
|
||||||
}
|
}
|
||||||
return classes
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.message-bubble {
|
/* * 气泡组件样式
|
||||||
display: flex;
|
* 完全复用原 index.vue 的 .msg-row, .bubble 及其子元素样式
|
||||||
align-items: flex-start;
|
*/
|
||||||
margin-bottom: 24rpx;
|
|
||||||
|
|
||||||
&.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 {
|
||||||
}
|
padding: 24rpx; border-radius: 32rpx; font-size: 30rpx; line-height: 1.6; position: relative; word-break: break-all; transition: all 0.3s ease;
|
||||||
|
|
||||||
.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-other {
|
&.bubble-other {
|
||||||
background: var(--bubble-other);
|
background: #ffffff; color: #1f2937; border-bottom-left-radius: 8rpx; box-shadow: 0 2rpx 4rpx rgba(0,0,0,0.05);
|
||||||
color: var(--text-primary);
|
.theme-dark & { background: #262626; color: #e7e5e4; border: 2rpx solid #292524; box-shadow: none; }
|
||||||
border-top-left-radius: 4rpx;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.bubble-media {
|
/* 自己消息 */
|
||||||
padding: 8rpx;
|
&.bubble-self {
|
||||||
background: transparent;
|
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>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,51 +1,125 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="message-input" :class="{ 'theme-dark': isDark }">
|
<view class="message-input-container" :class="{ 'theme-dark': isDark }">
|
||||||
<!-- @ 提及弹窗 (保持原有功能不变) -->
|
<!-- 悬浮输入条 -->
|
||||||
<wd-popup v-model="showMentionPicker" position="bottom" safe-area-inset-bottom>
|
<view class="input-floater">
|
||||||
<view class="mention-picker">
|
<!-- 1. @提醒选择器 -->
|
||||||
<view class="mention-header">
|
<mention-picker
|
||||||
<text class="title">选择要 @ 的成员</text>
|
v-if="isGroup"
|
||||||
<view class="close-btn" @click="showMentionPicker = false">
|
:show="showMentionPicker"
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
:users="mentionUsers"
|
||||||
<line x1="18" y1="6" x2="6" y2="18"/>
|
@select="handleMentionSelect"
|
||||||
<line x1="6" y1="6" x2="18" y2="18"/>
|
@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>
|
</svg>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<scroll-view scroll-y class="mention-list">
|
</view>
|
||||||
<view
|
</view>
|
||||||
v-for="member in members"
|
|
||||||
:key="member.user_id"
|
<!-- 4. 更多功能面板 -->
|
||||||
class="mention-item"
|
<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;">
|
||||||
@click="selectMention(member)"
|
<view class="tools-grid">
|
||||||
>
|
<view class="tool-item" @click="emitAction('send-image')">
|
||||||
<app-avatar
|
<view class="tool-icon album">
|
||||||
:src="member.user?.avatar"
|
<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>
|
||||||
:name="member.nickname || member.user?.name"
|
|
||||||
:size="72"
|
|
||||||
radius="8rpx"
|
|
||||||
/>
|
|
||||||
<text class="mention-name">{{ member.nickname || member.user?.name || '未知' }}</text>
|
|
||||||
</view>
|
</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>
|
</view>
|
||||||
</wd-popup>
|
</wd-popup>
|
||||||
|
|
||||||
<!-- 录音浮层 -->
|
<!-- 5. 录音遮罩 (保持业务逻辑) -->
|
||||||
<view v-if="isRecording" class="recording-overlay">
|
<view v-if="isRecording" class="recording-overlay">
|
||||||
<view class="recording-content" :class="{ 'cancel-mode': isCancelRecording }">
|
<view class="recording-content" :class="{ 'cancel-mode': isCancelRecording }">
|
||||||
<view class="recording-icon">
|
<view class="recording-icon">
|
||||||
<svg v-if="!isCancelRecording" viewBox="0 0 24 24" fill="currentColor">
|
<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>
|
||||||
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/>
|
<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>
|
||||||
<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>
|
||||||
<view class="wave-bars">
|
<view class="wave-bars">
|
||||||
<view class="wave-bar" v-for="i in 5" :key="i" :style="{ animationDelay: `${i * 0.1}s` }"></view>
|
<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>
|
<text class="recording-tip">{{ isCancelRecording ? '松开取消' : '上滑取消发送' }}</text>
|
||||||
</view>
|
</view>
|
||||||
</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>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import { useTheme } from '@/composables/useTheme'
|
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'
|
import type { GroupMember } from '@/types/api'
|
||||||
|
|
||||||
interface Props {
|
// Props & Emits
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
modelValue?: string
|
modelValue?: string
|
||||||
isGroup?: boolean
|
isGroup?: boolean
|
||||||
members?: GroupMember[]
|
members?: GroupMember[]
|
||||||
}
|
}>(), {
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
|
||||||
modelValue: '',
|
modelValue: '',
|
||||||
isGroup: false,
|
isGroup: false,
|
||||||
members: () => []
|
members: () => []
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update:modelValue', value: string): void
|
(e: 'update:modelValue', val: string): void
|
||||||
(e: 'send', value: string): void
|
(e: 'send', val: string): void
|
||||||
(e: 'sendVoice', data: { path: string; duration: number }): void
|
(e: 'send-voice', data: { path: string, duration: number }): void
|
||||||
(e: 'emoji'): void
|
(e: 'send-image'): void
|
||||||
(e: 'image'): void
|
(e: 'send-camera'): void
|
||||||
(e: 'file'): void
|
(e: 'send-file'): void
|
||||||
(e: 'more'): void
|
(e: 'focus'): void
|
||||||
(e: 'mention', member: GroupMember): void
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { isDark } = useTheme()
|
const { isDark } = useTheme()
|
||||||
|
|
||||||
// 文本输入相关
|
|
||||||
const inputValue = ref(props.modelValue)
|
const inputValue = ref(props.modelValue)
|
||||||
|
const showEmoji = ref(false)
|
||||||
|
const showMore = ref(false)
|
||||||
const showMentionPicker = ref(false)
|
const showMentionPicker = ref(false)
|
||||||
|
|
||||||
// 语音录制相关
|
// 录音状态
|
||||||
const isRecording = ref(false)
|
const isRecording = ref(false)
|
||||||
const isCancelRecording = ref(false)
|
const isCancelRecording = ref(false)
|
||||||
const recordDuration = ref(0)
|
const recordDuration = ref(0)
|
||||||
let recordTimer: ReturnType<typeof setInterval> | null = null
|
let recordTimer: any = null
|
||||||
let recorderManager: UniApp.RecorderManager | null = null
|
let recorderManager: any = null
|
||||||
let startY = 0
|
let recordStartY = 0
|
||||||
let voicePath = ''
|
|
||||||
|
|
||||||
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) {
|
function onInput(e: any) {
|
||||||
const value = e.detail?.value || e.value || inputValue.value
|
const val = e.detail?.value || inputValue.value
|
||||||
// 检测 @ 符号
|
if (props.isGroup && val.endsWith('@')) {
|
||||||
if (props.isGroup && value.endsWith('@')) {
|
|
||||||
showMentionPicker.value = true
|
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() {
|
function onSend() {
|
||||||
if (!inputValue.value.trim()) return
|
if (!inputValue.value.trim()) return
|
||||||
emit('send', inputValue.value.trim())
|
emit('send', inputValue.value.trim())
|
||||||
inputValue.value = ''
|
inputValue.value = ''
|
||||||
|
showEmoji.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 语音录制处理 ==========
|
function toggleEmoji() {
|
||||||
function initRecorderManager() {
|
showEmoji.value = !showEmoji.value
|
||||||
if (recorderManager) return recorderManager
|
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 = uni.getRecorderManager()
|
||||||
|
recorderManager.onStart(() => { isRecording.value = true; recordDuration.value = 0; recordTimer = setInterval(() => recordDuration.value++, 1000) })
|
||||||
recorderManager.onStart(() => {
|
recorderManager.onStop((res: any) => {
|
||||||
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) {
|
|
||||||
clearInterval(recordTimer)
|
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) {
|
function startRecording(e: TouchEvent) {
|
||||||
startY = e.touches[0].clientY
|
recordStartY = e.touches[0].clientY
|
||||||
isCancelRecording.value = false
|
if (!recorderManager) initRecorder()
|
||||||
|
recorderManager.start({ format: 'aac' })
|
||||||
// 初始化录音管理器
|
|
||||||
const recorder = initRecorderManager()
|
|
||||||
|
|
||||||
// 开始录音
|
|
||||||
recorder.start({
|
|
||||||
duration: 60000, // 最长60秒
|
|
||||||
sampleRate: 16000,
|
|
||||||
numberOfChannels: 1,
|
|
||||||
encodeBitRate: 48000,
|
|
||||||
format: 'mp3'
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onRecordingMove(e: TouchEvent) {
|
function onRecordingMove(e: TouchEvent) {
|
||||||
if (!isRecording.value) return
|
if (!isRecording.value) return
|
||||||
|
if (recordStartY - e.touches[0].clientY > 80) isCancelRecording.value = true
|
||||||
const currentY = e.touches[0].clientY
|
|
||||||
const diff = startY - currentY
|
|
||||||
|
|
||||||
// 上滑超过80px触发取消
|
|
||||||
isCancelRecording.value = diff > 80
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopRecording() {
|
function stopRecording() { recorderManager?.stop() }
|
||||||
if (!isRecording.value) return
|
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')}` }
|
||||||
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
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
// ==========================================
|
/* =================================================================
|
||||||
// 与设计稿 聊天页面.html 第116-123行 完全一致
|
Message Input Styling - Warm Stone Restoration
|
||||||
// ==========================================
|
基于 聊天页面.html 的设计规范
|
||||||
.message-input {
|
================================================================= */
|
||||||
// 亮色模式颜色 (与设计稿一致)
|
|
||||||
--input-bg: #f3f4f6; // 设计稿: bg-gray-100
|
.message-input-container {
|
||||||
--input-text: #1f2937; // 设计稿: text-gray-800
|
/* 颜色变量 - 严格对标 HTML */
|
||||||
--placeholder-color: #9ca3af; // 设计稿: placeholder-gray-400
|
--bg-floater: #ffffff;
|
||||||
--icon-color: #6b7280; // 设计稿: text-gray-500
|
--bg-input: #f3f4f6; /* gray-100 */
|
||||||
--icon-hover: #4F46E5; // 设计稿: hover:text-indigo-600
|
--text-input: #1f2937; /* gray-800 */
|
||||||
--send-bg: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%);
|
--text-placeholder: #9ca3af;/* gray-400 */
|
||||||
--border-color: #f3f4f6; // 设计稿: border-gray-100
|
--icon-normal: #6b7280; /* gray-500 */
|
||||||
--bg-content: #ffffff; // 设计稿: bg-white
|
--icon-hover: #4f46e5; /* indigo-600 */
|
||||||
|
--border-color: #f3f4f6; /* gray-100 */
|
||||||
position: relative;
|
|
||||||
background: var(--bg-content);
|
|
||||||
border-top: 2rpx solid var(--border-color);
|
|
||||||
transition: all 0.3s;
|
|
||||||
|
|
||||||
// 暗色模式 - Warm Stone (与设计稿一致)
|
|
||||||
&.theme-dark {
|
&.theme-dark {
|
||||||
--input-bg: #292524; // 设计稿: dark:bg-warm-800
|
--bg-floater: #1c1917; /* warm-900 */
|
||||||
--input-text: #f5f5f4; // 设计稿: dark:text-warm-100
|
--bg-input: #292524; /* warm-800 */
|
||||||
--placeholder-color: #78716c; // 设计稿: dark:placeholder-warm-500
|
--text-input: #e7e5e4; /* warm-100 */
|
||||||
--icon-color: #a8a29e; // 设计稿: dark:text-warm-400
|
--text-placeholder: #78716c;/* warm-500 */
|
||||||
--icon-hover: #f97316; // 设计稿: dark:hover:text-orange-400
|
--icon-normal: #a8a29e; /* warm-400 */
|
||||||
--send-bg: linear-gradient(135deg, #fb923c 0%, #f97316 100%);
|
--icon-hover: #fb923c; /* orange-400 */
|
||||||
--border-color: #292524; // 设计稿: dark:border-warm-800
|
--border-color: #292524; /* warm-800 */
|
||||||
--bg-content: #1c1917; // 设计稿: dark:bg-warm-900
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 底部输入栏 - 设计稿: px-4 py-3 pb-8 flex items-end gap-3
|
/* 底部悬浮条容器 */
|
||||||
.input-bar {
|
.input-floater {
|
||||||
display: flex;
|
position: fixed;
|
||||||
align-items: flex-end; // 设计稿: items-end
|
bottom: 0; left: 0; right: 0;
|
||||||
gap: 24rpx; // 设计稿: gap-3 = 12px = 24rpx
|
z-index: 200;
|
||||||
padding: 24rpx 32rpx; // 设计稿: px-4 py-3 = 16px 12px = 32rpx 24rpx
|
background: var(--bg-floater);
|
||||||
padding-bottom: calc(64rpx + env(safe-area-inset-bottom)); // 设计稿: pb-8 = 32px = 64rpx
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 语音按钮
|
/* 胶囊布局: Flex Row + Gap 3 (24rpx)
|
||||||
.voice-btn {
|
修改点:align-items: center 实现垂直居中
|
||||||
|
*/
|
||||||
|
.input-bar-capsule {
|
||||||
|
display: flex;
|
||||||
|
align-items: center; /* 核心修改: 垂直居中对齐 */
|
||||||
|
gap: 24rpx; /* gap-3 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图标按钮: w-7 h-7 (56rpx) */
|
||||||
|
.icon-btn {
|
||||||
width: 56rpx;
|
width: 56rpx;
|
||||||
height: 56rpx;
|
height: 56rpx;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
color: var(--icon-normal);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin-bottom: 16rpx;
|
margin-bottom: 0; /* 修改点: 移除底部 margin,因为已经是垂直居中了 */
|
||||||
color: var(--icon-color);
|
transition: color 0.2s, transform 0.1s;
|
||||||
transition: color 0.15s;
|
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
width: 56rpx;
|
width: 56rpx;
|
||||||
height: 56rpx;
|
height: 56rpx;
|
||||||
|
stroke-width: 2; /* 确保图标线条清晰 */
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
|
color: var(--icon-hover);
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.recording {
|
||||||
color: var(--icon-hover);
|
color: var(--icon-hover);
|
||||||
transform: scale(1.1);
|
transform: scale(1.1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 输入框容器 - 与设计稿完全一致
|
/* 输入框容器: 核心高度调整
|
||||||
// 设计稿: flex-1 bg-gray-100 dark:bg-warm-800 rounded-2xl px-4 py-2.5 min-h-[44px]
|
新设计: min-h-[36px] (72rpx) -> 精致
|
||||||
// border border-transparent dark:border-warm-700
|
*/
|
||||||
.input-wrap {
|
.input-field-wrap {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 88rpx; // 设计稿: min-h-[44px] = 44px = 88rpx
|
min-height: 72rpx;
|
||||||
background: var(--input-bg);
|
background: var(--bg-input);
|
||||||
border-radius: 32rpx; // 设计稿: rounded-2xl = 16px = 32rpx
|
border-radius: 36rpx; /* 圆角对应高度的一半 */
|
||||||
padding: 20rpx 32rpx; // 设计稿: px-4 py-2.5 = 16px 10px = 32rpx 20rpx
|
padding: 16rpx 28rpx;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border: 2rpx solid transparent; // 设计稿: border border-transparent
|
transition: background-color 0.3s;
|
||||||
transition: all 0.2s;
|
|
||||||
|
.text-input {
|
||||||
// 设计稿: dark:border-warm-700
|
width: 100%;
|
||||||
.theme-dark & {
|
max-height: 200rpx;
|
||||||
border-color: #44403c; // warm-700
|
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 {
|
.right-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 16rpx;
|
gap: 24rpx; /* gap-3 between smile and plus */
|
||||||
margin-bottom: 16rpx;
|
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 {
|
.send-btn {
|
||||||
width: 72rpx;
|
width: 72rpx;
|
||||||
height: 72rpx;
|
height: 72rpx;
|
||||||
border-radius: 50%;
|
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;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
box-shadow: 0 4rpx 16rpx rgba(79, 70, 229, 0.3);
|
margin-bottom: 0; /* 修改点: 移除底部 margin */
|
||||||
transition: all 0.2s;
|
box-shadow: 0 4rpx 12rpx rgba(79, 70, 229, 0.3);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
width: 36rpx;
|
width: 36rpx;
|
||||||
height: 36rpx;
|
height: 36rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
transform: scale(0.95);
|
transform: scale(0.95);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes popIn {
|
/* 录音浮层 (复用) */
|
||||||
from { transform: scale(0); }
|
|
||||||
to { transform: scale(1); }
|
|
||||||
}
|
|
||||||
|
|
||||||
// 录音浮层
|
|
||||||
.recording-overlay {
|
.recording-overlay {
|
||||||
position: fixed;
|
position: fixed; inset: 0; background: rgba(0,0,0,0.6); z-index: 999;
|
||||||
top: 0;
|
display: flex; align-items: center; justify-content: center;
|
||||||
left: 0;
|
.recording-content {
|
||||||
right: 0;
|
background: var(--bg-floater); padding: 64rpx; border-radius: 48rpx;
|
||||||
bottom: 0;
|
display: flex; flex-direction: column; align-items: center;
|
||||||
background: rgba(0, 0, 0, 0.6);
|
&.cancel-mode { .recording-icon { background: #ef4444; } }
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
.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 {
|
/* 更多面板 Grid (复用) */
|
||||||
width: 120rpx;
|
.tools-grid {
|
||||||
height: 120rpx;
|
padding: 48rpx 32rpx; display: grid; grid-template-columns: repeat(4, 1fr); gap: 32rpx;
|
||||||
border-radius: 50%;
|
.tool-item {
|
||||||
background: #10b981;
|
display: flex; flex-direction: column; align-items: center; gap: 16rpx;
|
||||||
display: flex;
|
.tool-icon {
|
||||||
align-items: center;
|
width: 112rpx; height: 112rpx; border-radius: 32rpx; display: flex; align-items: center; justify-content: center;
|
||||||
justify-content: center;
|
&.album { background: #10b981; } &.camera { background: #f97316; } &.file { background: #eab308; }
|
||||||
margin-bottom: 32rpx;
|
svg { width: 56rpx; height: 56rpx; }
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
text { font-size: 24rpx; color: var(--icon-normal); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</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>
|
<template>
|
||||||
<view class="detail-container" :class="{ 'theme-dark': isDark }">
|
<view class="detail-container" :class="{ 'theme-dark': isDark }">
|
||||||
<!-- 头部背景区 (视差效果) -->
|
<!-- 视差背景 (与设计稿一致: blur-xl scale-110 opacity-70) -->
|
||||||
<view class="profile-header">
|
<view class="parallax-bg">
|
||||||
<view class="blur-bg" :style="headerBgStyle" />
|
<image
|
||||||
<view class="gradient-mask" />
|
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">
|
<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">
|
<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"/>
|
<path d="M15 18l-6-6 6-6"/>
|
||||||
</svg>
|
</svg>
|
||||||
</view>
|
</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>
|
</view>
|
||||||
|
|
||||||
<!-- 主体内容 (浮动卡片) -->
|
<!-- 主体内容 -->
|
||||||
<scroll-view scroll-y class="main-content custom-scrollbar" v-if="!loading && contact">
|
<scroll-view scroll-y class="main-scroll custom-scrollbar" v-if="!loading && contact">
|
||||||
<view class="content-wrapper">
|
<!-- 身份卡片区 (与设计稿一致) -->
|
||||||
|
<view class="identity-section">
|
||||||
<!-- 用户身份卡片 -->
|
<!-- 头像: w-32 h-32 rounded-full border-[6px] border-white shadow-xl -->
|
||||||
<view class="identity-card animate-fade-in-up">
|
<view class="avatar-wrapper">
|
||||||
<view class="avatar-wrapper">
|
<app-avatar
|
||||||
<app-avatar
|
:src="contact.user?.avatar"
|
||||||
:src="contact.user?.avatar"
|
:name="contact.remark_name || contact.user?.name"
|
||||||
:name="contact.remark_name || contact.user?.name"
|
:size="256"
|
||||||
:size="256"
|
round
|
||||||
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>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 设置组:资料 -->
|
<!-- 名字: text-2xl font-bold -->
|
||||||
<view class="settings-group animate-fade-in-up" style="animation-delay: 100ms;">
|
<text class="user-name">{{ contact.remark_name || contact.user?.name || '未知' }}</text>
|
||||||
<view class="settings-card">
|
|
||||||
<view class="setting-item" @click="editRemark">
|
<!-- 标签: text-xs bg-gray-100 px-2 py-0.5 rounded -->
|
||||||
<view class="icon-wrap color-blue">
|
<view class="tag-row">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2">
|
<view class="info-tag">ID: {{ contact.user?.id || '-' }}</view>
|
||||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
<view class="info-tag">{{ contact.user?.region || '未知' }}</view>
|
||||||
<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>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 设置组:交互 -->
|
<!-- 签名: text-sm text-gray-500 text-center max-w-[200px] -->
|
||||||
<view class="settings-group animate-fade-in-up" style="animation-delay: 150ms;">
|
<text class="bio-text">{{ contact.user?.desc || '这个人很懒,什么都没写' }}</text>
|
||||||
<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" />
|
|
||||||
</view>
|
</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>
|
</scroll-view>
|
||||||
|
|
||||||
<!-- Loading -->
|
<!-- Loading -->
|
||||||
@@ -125,20 +104,23 @@
|
|||||||
<wd-loading size="60rpx" :color="isDark ? '#f97316' : '#4F46E5'"/>
|
<wd-loading size="60rpx" :color="isDark ? '#f97316' : '#4F46E5'"/>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 底部悬浮操作栏 -->
|
<!-- 底部悬浮操作栏 (与设计稿一致: rounded-t-[32px] shadow-[0_-10px_40px]) -->
|
||||||
<view class="floating-dock" v-if="contact">
|
<view class="floating-dock" v-if="contact">
|
||||||
<view class="dock-content">
|
<view class="dock-buttons">
|
||||||
|
<!-- 电话: flex-1 -->
|
||||||
<view class="dock-btn secondary" @click="startAudioCall">
|
<view class="dock-btn secondary" @click="startAudioCall">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<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>
|
</svg>
|
||||||
</view>
|
</view>
|
||||||
|
<!-- 发消息: flex-[2] -->
|
||||||
<view class="dock-btn primary" @click="goChat">
|
<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"/>
|
<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>
|
</svg>
|
||||||
<text>发消息</text>
|
<text>发消息</text>
|
||||||
</view>
|
</view>
|
||||||
|
<!-- 视频: flex-1 -->
|
||||||
<view class="dock-btn secondary" @click="startVideoCall">
|
<view class="dock-btn secondary" @click="startVideoCall">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<polygon points="23 7 16 12 23 17 23 7"/>
|
<polygon points="23 7 16 12 23 17 23 7"/>
|
||||||
@@ -146,8 +128,9 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="danger-btn" @click="deleteFriend">
|
<!-- 删除好友: text-xs text-gray-400 underline -->
|
||||||
|
<view class="delete-link" @click="deleteFriend">
|
||||||
<text>删除好友</text>
|
<text>删除好友</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -158,7 +141,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, computed } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { onLoad } from '@dcloudio/uni-app'
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
import * as contactApi from '@/api/modules/contact'
|
import * as contactApi from '@/api/modules/contact'
|
||||||
import { useToast, useMessage } from 'wot-design-uni'
|
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 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 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 || '' })
|
onLoad((options: any) => { contactId.value = options?.id || ''; userId.value = options?.userId || '' })
|
||||||
onMounted(async () => { if (contactId.value) { await loadContact() } else if (userId.value) { await loadContactByUserId() } })
|
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 } }
|
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 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 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 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('删除失败') } } }
|
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>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
// ==========================================
|
||||||
// 页面容器 - 浅色模式 (与设计稿完全一致)
|
// 页面容器 - 浅色模式 (与设计稿完全一致)
|
||||||
|
// ==========================================
|
||||||
.detail-container {
|
.detail-container {
|
||||||
--bg-base: #ffffff; // 白色页面背景
|
--bg-page: #f3f4f6; // gray-100 (页面背景)
|
||||||
--bg-card: #ffffff; // 卡片背景
|
--bg-card: #ffffff; // 卡片背景
|
||||||
--bg-secondary: #f3f4f6; // gray-100 (次要背景/按钮)
|
--bg-tag: #f3f4f6; // gray-100 (标签背景)
|
||||||
--text-main: #1f2937; // gray-900 (名称)
|
--bg-toggle: #e5e7eb; // gray-200 (开关关闭)
|
||||||
--text-sub: #6b7280; // gray-500 (签名/标签)
|
--text-main: #111827; // gray-900 (名称)
|
||||||
--text-tertiary: #9ca3af; // gray-400 (值)
|
--text-sub: #6b7280; // gray-500 (签名)
|
||||||
|
--text-tertiary: #9ca3af; // gray-400 (标签/值)
|
||||||
--color-brand: #4F46E5; // indigo-600
|
--color-brand: #4F46E5; // indigo-600
|
||||||
--border-color: #f9fafb; // gray-50
|
--border-color: #f9fafb; // gray-50
|
||||||
--gradient-to: #ffffff; // 渐变终点色
|
--gradient-to: #ffffff; // 渐变终点色
|
||||||
|
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: var(--bg-base);
|
background: var(--bg-page);
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
// 深色模式 - Warm Stone (与设计稿完全一致)
|
// 深色模式 - Warm Stone (与设计稿完全一致)
|
||||||
&.theme-dark {
|
&.theme-dark {
|
||||||
--bg-base: #0c0a09; // warm-950 (极深背景)
|
--bg-page: #0c0a09; // warm-950
|
||||||
--bg-card: #1c1917; // warm-900
|
--bg-card: #1c1917; // warm-900
|
||||||
--bg-secondary: #292524; // warm-800
|
--bg-tag: #292524; // warm-800
|
||||||
--text-main: #f5f5f4; // warm-100
|
--bg-toggle: #44403c; // warm-700
|
||||||
|
--text-main: #fafaf9; // warm-50
|
||||||
--text-sub: #a8a29e; // warm-400
|
--text-sub: #a8a29e; // warm-400
|
||||||
--text-tertiary: #78716c; // warm-500
|
--text-tertiary: #78716c; // warm-500
|
||||||
--color-brand: #f97316; // orange-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
|
--gradient-to: #0c0a09; // warm-950
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 动画
|
// 动画
|
||||||
@keyframes fadeInUp {
|
@keyframes fadeInUp {
|
||||||
from {
|
from { opacity: 0; transform: translateY(20rpx); }
|
||||||
opacity: 0;
|
to { opacity: 1; transform: translateY(0); }
|
||||||
transform: translateY(20rpx);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-fade-in-up {
|
.animate-fade-in-up {
|
||||||
@@ -244,304 +223,350 @@ async function deleteFriend() { try { await messageBox.confirm({ title: '提示'
|
|||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 视差头部背景
|
// ==========================================
|
||||||
.profile-header {
|
// 视差背景 (与设计稿一致: blur-xl scale-110 opacity-70)
|
||||||
|
// ==========================================
|
||||||
|
.parallax-bg {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 600rpx;
|
height: 600rpx;
|
||||||
z-index: 0;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
z-index: 0;
|
||||||
|
|
||||||
.blur-bg {
|
.blur-image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-size: cover;
|
// blur-xl = 24px, 需要用px单位
|
||||||
background-position: center;
|
filter: blur(24px);
|
||||||
filter: blur(40px);
|
-webkit-filter: blur(24px);
|
||||||
transform: scale(1.1);
|
transform: scale(1.1); // scale-110
|
||||||
opacity: 0.7;
|
opacity: 0.7; // opacity-70
|
||||||
|
|
||||||
.theme-dark & {
|
.theme-dark & {
|
||||||
opacity: 0.4;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.gradient-mask {
|
.gradient-overlay {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background: linear-gradient(to bottom, transparent 0%, var(--gradient-to) 85%);
|
background: linear-gradient(to bottom, transparent 0%, var(--gradient-to) 100%);
|
||||||
}
|
|
||||||
|
|
||||||
.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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 主内容
|
// ==========================================
|
||||||
.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;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-wrapper {
|
// ==========================================
|
||||||
padding: 0 32rpx;
|
// 身份卡片区 (与设计稿完全一致)
|
||||||
padding-top: calc(220rpx + var(--status-bar-height));
|
// ==========================================
|
||||||
}
|
.identity-section {
|
||||||
|
|
||||||
// 身份卡片
|
|
||||||
.identity-card {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 60rpx;
|
padding: 64rpx 48rpx 64rpx;
|
||||||
|
|
||||||
|
// 头像: w-32 h-32 rounded-full border-[6px] border-white shadow-xl
|
||||||
.avatar-wrapper {
|
.avatar-wrapper {
|
||||||
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.15);
|
|
||||||
border-radius: 50%;
|
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 {
|
// 名字: text-2xl font-bold
|
||||||
margin-top: 30rpx;
|
.user-name {
|
||||||
text-align: center;
|
font-size: 48rpx; // text-2xl = 24px = 48rpx
|
||||||
|
font-weight: 700; // font-bold
|
||||||
.user-name {
|
color: var(--text-main);
|
||||||
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);
|
|
||||||
margin-bottom: 16rpx;
|
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 {
|
.settings-card {
|
||||||
|
margin: 0 24rpx 48rpx;
|
||||||
background: var(--bg-card);
|
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;
|
overflow: hidden;
|
||||||
|
|
||||||
|
.theme-dark & {
|
||||||
|
box-shadow: none;
|
||||||
|
border-color: rgba(41, 37, 36, 0.5);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.setting-item {
|
// 设置行 (与设计稿一致)
|
||||||
|
.setting-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 28rpx 28rpx;
|
justify-content: space-between;
|
||||||
border-bottom: 1rpx solid rgba(0, 0, 0, 0.04);
|
padding: 32rpx; // p-4 = 16px = 32rpx
|
||||||
|
border-bottom: 2rpx solid var(--border-color);
|
||||||
transition: background 0.15s;
|
transition: background 0.15s;
|
||||||
|
|
||||||
.theme-dark & {
|
.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;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
background: rgba(0, 0, 0, 0.02);
|
background: rgba(0, 0, 0, 0.02);
|
||||||
|
|
||||||
.theme-dark & {
|
.theme-dark & {
|
||||||
background: rgba(255, 255, 255, 0.02);
|
background: rgba(255, 255, 255, 0.02);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-wrap {
|
.row-label {
|
||||||
width: 60rpx;
|
font-size: 28rpx; // text-sm = 14px = 28rpx
|
||||||
height: 60rpx;
|
font-weight: 500; // font-medium
|
||||||
border-radius: 16rpx;
|
color: var(--text-main);
|
||||||
|
|
||||||
|
.theme-dark & {
|
||||||
|
color: #e7e5e4; // warm-200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-right {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
gap: 8rpx;
|
||||||
margin-right: 24rpx;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
width: 32rpx;
|
|
||||||
height: 32rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.color-blue { background: #0ea5e9; }
|
|
||||||
&.color-orange { background: #f97316; }
|
|
||||||
&.color-purple { background: #8b5cf6; }
|
|
||||||
&.color-red { background: #ef4444; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-body {
|
.row-value {
|
||||||
flex: 1;
|
font-size: 24rpx; // text-xs
|
||||||
display: flex;
|
color: var(--text-tertiary);
|
||||||
flex-direction: column;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-title {
|
.chevron-icon {
|
||||||
font-size: 30rpx;
|
|
||||||
color: var(--text-main);
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-value {
|
|
||||||
font-size: 26rpx;
|
|
||||||
color: var(--text-sub);
|
|
||||||
margin-top: 4rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chevron {
|
|
||||||
width: 32rpx;
|
width: 32rpx;
|
||||||
height: 32rpx;
|
height: 32rpx;
|
||||||
color: var(--text-sub);
|
color: #d1d5db; // gray-300
|
||||||
opacity: 0.5;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.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 {
|
.floating-dock {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
padding: 30rpx 40rpx;
|
padding: 32rpx 48rpx; // px-6 py-4
|
||||||
padding-bottom: calc(30rpx + env(safe-area-inset-bottom));
|
padding-bottom: calc(64rpx + env(safe-area-inset-bottom)); // pb-8
|
||||||
background: var(--bg-card);
|
background: var(--bg-card);
|
||||||
border-top: 1rpx solid rgba(0, 0, 0, 0.05);
|
border-top: 2rpx solid var(--border-color);
|
||||||
border-radius: 64rpx 64rpx 0 0;
|
border-radius: 64rpx 64rpx 0 0; // rounded-t-[32px] = 64rpx
|
||||||
box-shadow: 0 -20rpx 80rpx rgba(0, 0, 0, 0.05);
|
box-shadow: 0 -20rpx 80rpx rgba(0, 0, 0, 0.05); // shadow-[0_-10px_40px]
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
|
|
||||||
.theme-dark & {
|
.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 {
|
&:active {
|
||||||
display: flex;
|
transform: scale(0.95);
|
||||||
align-items: center;
|
opacity: 0.9;
|
||||||
gap: 24rpx;
|
}
|
||||||
margin-bottom: 24rpx;
|
|
||||||
|
|
||||||
.dock-btn {
|
// 次要按钮: flex-1
|
||||||
height: 100rpx;
|
&.secondary {
|
||||||
border-radius: 32rpx;
|
flex: 1;
|
||||||
display: flex;
|
background: var(--bg-tag);
|
||||||
align-items: center;
|
color: var(--text-main);
|
||||||
justify-content: center;
|
|
||||||
transition: all 0.15s;
|
|
||||||
|
|
||||||
svg {
|
.theme-dark & {
|
||||||
width: 44rpx;
|
background: #292524; // warm-800
|
||||||
height: 44rpx;
|
color: #e7e5e4; // warm-200
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:hover {
|
||||||
transform: scale(0.96);
|
background: #e5e7eb; // gray-200
|
||||||
}
|
.theme-dark & {
|
||||||
|
background: #44403c; // warm-700
|
||||||
&.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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.danger-btn {
|
// 主要按钮: flex-[2]
|
||||||
text-align: center;
|
&.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 {
|
svg {
|
||||||
color: var(--text-sub);
|
stroke: #fff;
|
||||||
font-size: 26rpx;
|
|
||||||
padding: 10rpx 20rpx;
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active text {
|
.theme-dark & {
|
||||||
opacity: 1;
|
background: linear-gradient(135deg, #fb923c 0%, #ea580c 100%);
|
||||||
color: #ef4444;
|
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 {
|
.center-loading {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
@@ -549,3 +574,4 @@ async function deleteFriend() { try { await messageBox.confirm({ title: '提示'
|
|||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -32,22 +32,6 @@
|
|||||||
</view>
|
</view>
|
||||||
</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. 会话列表 -->
|
<!-- 3. 会话列表 -->
|
||||||
<scroll-view
|
<scroll-view
|
||||||
class="conversation-list custom-scrollbar"
|
class="conversation-list custom-scrollbar"
|
||||||
@@ -56,6 +40,22 @@
|
|||||||
:refresher-triggered="refreshing"
|
:refresher-triggered="refreshing"
|
||||||
@refresherrefresh="onRefresh"
|
@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">
|
<view v-if="loading && conversations.length === 0" class="loading-state">
|
||||||
<wd-loading size="50rpx" color="var(--color-primary)" />
|
<wd-loading size="50rpx" color="var(--color-primary)" />
|
||||||
<text>连接中...</text>
|
<text>连接中...</text>
|
||||||
|
|||||||
Reference in New Issue
Block a user