UI设计
This commit is contained in:
3
components.d.ts
vendored
3
components.d.ts
vendored
@@ -8,6 +8,7 @@ export {}
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
AppAvatar: typeof import('./src/components/common/AppAvatar.vue')['default']
|
||||
AppDrawer: typeof import('./src/components/common/AppDrawer.vue')['default']
|
||||
AppEmpty: typeof import('./src/components/common/AppEmpty.vue')['default']
|
||||
AppFooter: typeof import('./src/components/AppFooter.vue')['default']
|
||||
AppLoading: typeof import('./src/components/common/AppLoading.vue')['default']
|
||||
@@ -15,6 +16,7 @@ declare module 'vue' {
|
||||
AppNavBar: typeof import('./src/components/common/AppNavBar.vue')['default']
|
||||
AppTabBar: typeof import('./src/components/common/AppTabBar.vue')['default']
|
||||
CallWindow: typeof import('./src/components/call/CallWindow.vue')['default']
|
||||
GlobalCallProvider: typeof import('./src/components/call/GlobalCallProvider.vue')['default']
|
||||
GroupCallBanner: typeof import('./src/components/call/GroupCallBanner.vue')['default']
|
||||
GroupCallIncoming: typeof import('./src/components/call/GroupCallIncoming.vue')['default']
|
||||
GroupCallModal: typeof import('./src/components/call/GroupCallModal.vue')['default']
|
||||
@@ -28,6 +30,7 @@ declare module 'vue' {
|
||||
MomentCommentInput: typeof import('./src/components/moment/MomentCommentInput.vue')['default']
|
||||
MomentCommentList: typeof import('./src/components/moment/MomentCommentList.vue')['default']
|
||||
MomentNotificationPanel: typeof import('./src/components/moment/MomentNotificationPanel.vue')['default']
|
||||
PlusMenu: typeof import('./src/components/common/PlusMenu.vue')['default']
|
||||
SelectContactsModal: typeof import('./src/components/chat/SelectContactsModal.vue')['default']
|
||||
UserInfoCard: typeof import('./src/components/common/UserInfoCard.vue')['default']
|
||||
UserMomentsModal: typeof import('./src/components/moment/UserMomentsModal.vue')['default']
|
||||
|
||||
@@ -3,11 +3,13 @@ import { onLaunch, onShow } from '@dcloudio/uni-app'
|
||||
import { useTheme } from '@/composables/useTheme'
|
||||
import { useAuthStore, useConversationStore } from '@/stores'
|
||||
import { wsManager } from '@/api/websocket'
|
||||
import { useGroupWebRTC } from '@/composables/useGroupWebRTC'
|
||||
import type { ChatMessage } from '@/types/api'
|
||||
|
||||
const { initTheme } = useTheme()
|
||||
const authStore = useAuthStore()
|
||||
const conversationStore = useConversationStore()
|
||||
const groupWebRTC = useGroupWebRTC()
|
||||
|
||||
// 全局消息处理器
|
||||
function handleGlobalMessage(message: ChatMessage) {
|
||||
@@ -28,6 +30,8 @@ async function initWebSocket() {
|
||||
await wsManager.connect(userId)
|
||||
// 注册全局消息处理器
|
||||
wsManager.onMessage(handleGlobalMessage)
|
||||
// 初始化群通话信令监听器
|
||||
groupWebRTC.initListener()
|
||||
console.log('✅ WebSocket 全局初始化成功')
|
||||
} catch (error) {
|
||||
console.error('WebSocket 初始化失败:', error)
|
||||
|
||||
@@ -1,27 +1,36 @@
|
||||
<template>
|
||||
<!-- #ifdef H5 -->
|
||||
<view v-if="call.active" class="call-window" :class="{ minimized: call.minimized }">
|
||||
<!-- 最小化状态 -->
|
||||
<view v-if="call.minimized" class="minimized-bar" @click="toggleMinimize">
|
||||
<wd-icon :name="call.type === 'video' ? 'video' : 'phone'" size="32rpx" />
|
||||
<text class="call-duration">{{ formatDuration(call.duration) }}</text>
|
||||
<wd-icon name="expand" size="32rpx" />
|
||||
<!-- 最小化悬浮窗 -->
|
||||
<view v-if="call.minimized" class="minimized-bar" @click="$emit('toggleMinimize')">
|
||||
<view class="pulse-dot"></view>
|
||||
<svg class="mini-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path v-if="call.type === 'video'" d="M23 7l-7 5 7 5V7z M16 5H3a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h13a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2z"/>
|
||||
<path v-else 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.84 12.84 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.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/>
|
||||
</svg>
|
||||
<text class="mini-duration">{{ formatDuration(call.duration) }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 完整窗口 -->
|
||||
<view v-else class="full-window">
|
||||
<!-- 顶部控制栏 -->
|
||||
<view class="header">
|
||||
<view class="status-text">{{ call.statusText }}</view>
|
||||
<view v-if="call.status === 'connected'" class="duration">
|
||||
{{ formatDuration(call.duration) }}
|
||||
</view>
|
||||
<wd-icon name="arrow-down" size="40rpx" class="minimize-btn" @click="toggleMinimize" />
|
||||
<!-- 模糊背景 -->
|
||||
<view class="blur-bg">
|
||||
<image v-if="call.callerAvatar" :src="resolveImageUrl(call.callerAvatar)" class="blur-img" mode="aspectFill" />
|
||||
<view class="blur-overlay"></view>
|
||||
</view>
|
||||
|
||||
<!-- 最小化按钮 (右上角) -->
|
||||
<view class="minimize-btn" @click="$emit('toggleMinimize')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="4 14 10 14 10 20"/>
|
||||
<polyline points="20 10 14 10 14 4"/>
|
||||
<line x1="14" y1="10" x2="21" y2="3"/>
|
||||
<line x1="3" y1="21" x2="10" y2="14"/>
|
||||
</svg>
|
||||
</view>
|
||||
|
||||
<!-- 视频区域 -->
|
||||
<view v-if="call.type === 'video'" class="video-area">
|
||||
<!-- 远程视频 -->
|
||||
<video
|
||||
v-if="remoteStream"
|
||||
ref="remoteVideoRef"
|
||||
@@ -30,11 +39,14 @@
|
||||
playsinline
|
||||
/>
|
||||
<view v-else class="video-placeholder">
|
||||
<app-avatar :src="call.callerAvatar" :name="call.callerName" :size="160" />
|
||||
<text class="name">{{ call.callerName || '对方' }}</text>
|
||||
<view class="avatar-section">
|
||||
<view class="avatar-ring ring-1"></view>
|
||||
<view class="avatar-ring ring-2"></view>
|
||||
<app-avatar :src="call.callerAvatar" :name="call.callerName" :size="240" round />
|
||||
</view>
|
||||
<text class="caller-name">{{ call.callerName || '对方' }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 本地视频(小窗口) -->
|
||||
<video
|
||||
v-if="localStream && !call.camOff"
|
||||
ref="localVideoRef"
|
||||
@@ -45,41 +57,93 @@
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 语音通话 - 头像显示 -->
|
||||
<!-- 语音通话 / 视频占位 -->
|
||||
<view v-else class="audio-area">
|
||||
<app-avatar :src="call.callerAvatar" :name="call.callerName" :size="200" />
|
||||
<view class="avatar-section">
|
||||
<view class="avatar-ring ring-ping"></view>
|
||||
<view class="avatar-container">
|
||||
<app-avatar :src="call.callerAvatar" :name="call.callerName" :size="256" round />
|
||||
</view>
|
||||
</view>
|
||||
<text class="caller-name">{{ call.callerName || '对方' }}</text>
|
||||
<text class="call-status-text">{{ call.statusText }}</text>
|
||||
<text v-if="call.status === 'connected'" class="call-duration">{{ formatDuration(call.duration) }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 底部控制按钮 -->
|
||||
<view class="controls">
|
||||
<!-- 来电状态 - 接听/拒绝 -->
|
||||
<template v-if="call.status === 'incoming'">
|
||||
<view class="control-btn decline" @click="$emit('reject')">
|
||||
<wd-icon name="phone" size="48rpx" color="#fff" />
|
||||
<view class="action-item" @click="$emit('reject')">
|
||||
<view class="control-btn decline">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2">
|
||||
<path d="M23 7l-7 5 7 5V7z M16 5H3a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h13a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2z"/>
|
||||
</svg>
|
||||
</view>
|
||||
<text class="action-label">拒绝</text>
|
||||
</view>
|
||||
<view class="control-btn accept" @click="$emit('accept')">
|
||||
<wd-icon name="phone" size="48rpx" color="#fff" />
|
||||
<view class="action-item" @click="$emit('accept')">
|
||||
<view class="control-btn accept">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" 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 12.84 12.84 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.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/>
|
||||
</svg>
|
||||
</view>
|
||||
<text class="action-label">接听</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 通话中状态 -->
|
||||
<template v-else-if="call.status === 'connected'">
|
||||
<view class="control-btn" :class="{ active: call.muted }" @click="$emit('toggleMute')">
|
||||
<wd-icon :name="call.muted ? 'mute' : 'audio'" size="40rpx" />
|
||||
<view class="action-item" @click="$emit('toggleMute')">
|
||||
<view class="control-btn" :class="{ active: call.muted }">
|
||||
<svg v-if="call.muted" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="1" y1="1" x2="23" y2="23"/>
|
||||
<path d="M9 9v3a3 3 0 0 0 5.12 2.12M15 9.34V4a3 3 0 0 0-5.94-.6"/>
|
||||
<path d="M17 16.95A7 7 0 0 1 5 12v-2m14 0v2a7 7 0 0 1-.11 1.23"/>
|
||||
<line x1="12" y1="19" x2="12" y2="23"/>
|
||||
<line x1="8" y1="23" x2="16" y2="23"/>
|
||||
</svg>
|
||||
<svg v-else viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<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>
|
||||
<text class="action-label">{{ call.muted ? '已静音' : '静音' }}</text>
|
||||
</view>
|
||||
<view v-if="call.type === 'video'" class="control-btn" :class="{ active: call.camOff }" @click="$emit('toggleCamera')">
|
||||
<wd-icon :name="call.camOff ? 'video-off' : 'video'" size="40rpx" />
|
||||
<view v-if="call.type === 'video'" class="action-item" @click="$emit('toggleCamera')">
|
||||
<view class="control-btn" :class="{ active: call.camOff }">
|
||||
<svg v-if="call.camOff" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M16 16v1a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h2m5.66 0H14a2 2 0 0 1 2 2v3.34l1 1L23 7v10"/>
|
||||
<line x1="1" y1="1" x2="23" y2="23"/>
|
||||
</svg>
|
||||
<svg v-else viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polygon points="23 7 16 12 23 17 23 7"/>
|
||||
<rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>
|
||||
</svg>
|
||||
</view>
|
||||
<text class="action-label">{{ call.camOff ? '已关闭' : '摄像头' }}</text>
|
||||
</view>
|
||||
<view class="control-btn hangup" @click="$emit('end')">
|
||||
<wd-icon name="phone" size="48rpx" color="#fff" />
|
||||
<view class="action-item" @click="$emit('end')">
|
||||
<view class="control-btn hangup">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2">
|
||||
<path d="M23 7l-7 5 7 5V7z M16 5H3a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h13a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2z"/>
|
||||
</svg>
|
||||
</view>
|
||||
<text class="action-label">挂断</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 呼叫中状态 -->
|
||||
<template v-else-if="call.status === 'outgoing'">
|
||||
<view class="control-btn hangup" @click="$emit('end')">
|
||||
<wd-icon name="phone" size="48rpx" color="#fff" />
|
||||
<view class="action-item" @click="$emit('end')">
|
||||
<view class="control-btn hangup">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2">
|
||||
<path d="M23 7l-7 5 7 5V7z M16 5H3a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h13a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2z"/>
|
||||
</svg>
|
||||
</view>
|
||||
<text class="action-label">挂断</text>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
@@ -98,6 +162,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, nextTick } from 'vue'
|
||||
import AppAvatar from '@/components/common/AppAvatar.vue'
|
||||
import { resolveImageUrl } from '@/utils/image'
|
||||
import type { CallState } from '@/composables/useWebRTC'
|
||||
|
||||
interface Props {
|
||||
@@ -118,11 +183,9 @@ defineEmits<{
|
||||
(e: 'toggleMinimize'): void
|
||||
}>()
|
||||
|
||||
// Video 元素引用
|
||||
const localVideoRef = ref<HTMLVideoElement | null>(null)
|
||||
const remoteVideoRef = ref<HTMLVideoElement | null>(null)
|
||||
|
||||
// 监听 localStream 变化,设置 srcObject
|
||||
watch(() => props.localStream, (stream) => {
|
||||
// #ifdef H5
|
||||
nextTick(() => {
|
||||
@@ -133,7 +196,6 @@ watch(() => props.localStream, (stream) => {
|
||||
// #endif
|
||||
}, { immediate: true })
|
||||
|
||||
// 监听 remoteStream 变化,设置 srcObject
|
||||
watch(() => props.remoteStream, (stream) => {
|
||||
// #ifdef H5
|
||||
nextTick(() => {
|
||||
@@ -143,10 +205,6 @@ watch(() => props.remoteStream, (stream) => {
|
||||
})
|
||||
// #endif
|
||||
}, { immediate: true })
|
||||
|
||||
function toggleMinimize() {
|
||||
// 注意:这里需要通过 emit 触发,因为状态在父组件管理
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -157,30 +215,54 @@ function toggleMinimize() {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 9999;
|
||||
background: rgba(0, 0, 0, 0.95);
|
||||
background: #111;
|
||||
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
|
||||
&.minimized {
|
||||
top: auto;
|
||||
bottom: calc(120rpx + env(safe-area-inset-bottom));
|
||||
left: 20rpx;
|
||||
right: auto;
|
||||
width: 280rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 40rpx;
|
||||
background: var(--color-primary);
|
||||
top: calc(var(--status-bar-height, 0) + 240rpx);
|
||||
right: 32rpx;
|
||||
left: auto;
|
||||
bottom: auto;
|
||||
width: 200rpx;
|
||||
height: 280rpx;
|
||||
border-radius: 32rpx;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.15);
|
||||
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.4);
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.minimized-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 24rpx;
|
||||
color: #fff;
|
||||
background: #2c2c2e;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16rpx;
|
||||
cursor: pointer;
|
||||
|
||||
.call-duration {
|
||||
font-size: 28rpx;
|
||||
.pulse-dot {
|
||||
position: absolute;
|
||||
bottom: 16rpx;
|
||||
width: 8rpx;
|
||||
height: 8rpx;
|
||||
background: #10b981;
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.mini-icon {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.mini-duration {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-family: monospace;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,37 +270,61 @@ function toggleMinimize() {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header {
|
||||
.blur-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
|
||||
.blur-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
opacity: 0.4;
|
||||
filter: blur(40rpx);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.blur-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(180deg, rgba(0,0,0,0.4) 0%, transparent 30%, rgba(0,0,0,0.9) 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.minimize-btn {
|
||||
position: absolute;
|
||||
top: calc(28rpx + var(--status-bar-height, 0));
|
||||
right: 40rpx;
|
||||
z-index: 20;
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(8px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40rpx;
|
||||
padding-top: calc(40rpx + var(--status-bar-height, 0));
|
||||
position: relative;
|
||||
transition: all 0.15s;
|
||||
|
||||
.status-text {
|
||||
font-size: 32rpx;
|
||||
svg {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.duration {
|
||||
margin-left: 20rpx;
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.minimize-btn {
|
||||
position: absolute;
|
||||
right: 40rpx;
|
||||
color: #fff;
|
||||
&:active {
|
||||
transform: scale(0.9);
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.video-area {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
|
||||
.remote-video {
|
||||
width: 100%;
|
||||
@@ -226,88 +332,169 @@ function toggleMinimize() {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.video-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 30rpx;
|
||||
|
||||
.name {
|
||||
font-size: 36rpx;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.local-video {
|
||||
position: absolute;
|
||||
top: 40rpx;
|
||||
right: 40rpx;
|
||||
width: 200rpx;
|
||||
height: 280rpx;
|
||||
border-radius: 16rpx;
|
||||
border-radius: 24rpx;
|
||||
object-fit: cover;
|
||||
border: 4rpx solid rgba(255, 255, 255, 0.3);
|
||||
border: 4rpx solid rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.3);
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
}
|
||||
|
||||
.audio-area {
|
||||
.video-placeholder, .audio-area {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 30rpx;
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
padding-top: 200rpx;
|
||||
animation: fadeInUp 0.6s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.caller-name {
|
||||
font-size: 40rpx;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
.avatar-section {
|
||||
position: relative;
|
||||
margin-bottom: 48rpx;
|
||||
|
||||
.avatar-ring {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.1);
|
||||
|
||||
&.ring-ping {
|
||||
inset: -4rpx;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.2);
|
||||
animation: ping 2s cubic-bezier(0, 0, 0.2, 1) infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-container {
|
||||
border: 4rpx solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 50%;
|
||||
padding: 4rpx;
|
||||
box-shadow: 0 40rpx 100rpx rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
.caller-name {
|
||||
font-size: 60rpx;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
letter-spacing: -1rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.call-status-text {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.5rpx;
|
||||
}
|
||||
|
||||
.call-duration {
|
||||
font-size: 40rpx;
|
||||
font-family: monospace;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
|
||||
.controls {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 60rpx;
|
||||
gap: 80rpx;
|
||||
padding: 60rpx;
|
||||
padding-bottom: calc(60rpx + env(safe-area-inset-bottom));
|
||||
padding-bottom: calc(128rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.action-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
width: 128rpx;
|
||||
height: 128rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(8px);
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
transition: all 0.3s;
|
||||
|
||||
svg {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
}
|
||||
|
||||
&:active { transform: scale(0.95); }
|
||||
|
||||
&.active {
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
background: #fff;
|
||||
color: #000;
|
||||
border-color: #fff;
|
||||
}
|
||||
|
||||
&.accept {
|
||||
background: #07c160;
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
background: #10b981;
|
||||
border: none;
|
||||
box-shadow: 0 0 80rpx rgba(16, 185, 129, 0.4);
|
||||
|
||||
svg {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
}
|
||||
|
||||
&:active { background: #059669; }
|
||||
}
|
||||
|
||||
&.decline,
|
||||
&.hangup {
|
||||
background: #fa5151;
|
||||
transform: rotate(135deg);
|
||||
&.decline {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
|
||||
&:active { background: #ef4444; }
|
||||
}
|
||||
|
||||
&.hangup {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
background: #ef4444;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 8rpx 40rpx rgba(239, 68, 68, 0.3);
|
||||
|
||||
svg {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
}
|
||||
|
||||
&:active { background: #dc2626; }
|
||||
}
|
||||
}
|
||||
|
||||
.action-label {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.call-unsupported {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
inset: 0;
|
||||
z-index: 9999;
|
||||
background: var(--bg-page);
|
||||
display: flex;
|
||||
@@ -321,5 +508,19 @@ function toggleMinimize() {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
@keyframes ping {
|
||||
0% { transform: scale(1); opacity: 0.3; }
|
||||
75%, 100% { transform: scale(1.3); opacity: 0; }
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from { transform: translateY(40rpx); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -9,14 +9,19 @@
|
||||
<view class="call-icon">
|
||||
<view class="pulse-ring"></view>
|
||||
<view class="icon-inner">
|
||||
<wd-icon name="video" size="32rpx" color="#fff" />
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2">
|
||||
<polygon points="23 7 16 12 23 17 23 7"/>
|
||||
<rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>
|
||||
</svg>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="call-info">
|
||||
<view class="call-title">
|
||||
<text class="title-text">群通话进行中</text>
|
||||
<text class="participant-count">{{ participantsCount }}人</text>
|
||||
<text class="title-text">通话进行中</text>
|
||||
<view class="participant-count">
|
||||
<text>{{ participantsCount }}人</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 头像堆叠 -->
|
||||
@@ -25,13 +30,13 @@
|
||||
v-for="(user, index) in previewParticipants"
|
||||
:key="user.userId"
|
||||
class="stacked-avatar"
|
||||
:style="{ marginLeft: index > 0 ? '-16rpx' : '0' }"
|
||||
:style="{ marginLeft: index > 0 ? '-24rpx' : '0', zIndex: 10 - index }"
|
||||
>
|
||||
<app-avatar
|
||||
:src="user.avatar"
|
||||
:name="user.name"
|
||||
:size="40"
|
||||
radius="50%"
|
||||
round
|
||||
/>
|
||||
</view>
|
||||
<view
|
||||
@@ -44,14 +49,9 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<wd-button
|
||||
type="success"
|
||||
size="small"
|
||||
@click="handleJoin"
|
||||
>
|
||||
<wd-icon name="phone" size="28rpx" />
|
||||
<text style="margin-left: 8rpx;">加入</text>
|
||||
</wd-button>
|
||||
<view class="join-btn" @click="handleJoin">
|
||||
<text>加入</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -86,15 +86,32 @@ function handleJoin() {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.group-call-banner {
|
||||
margin: 16rpx 24rpx;
|
||||
margin: 24rpx 32rpx;
|
||||
padding: 24rpx;
|
||||
background: var(--bg-content);
|
||||
border: 1rpx solid var(--divider-color);
|
||||
border-radius: 24rpx;
|
||||
background: #fff;
|
||||
border: 1rpx solid rgba(0, 0, 0, 0.05);
|
||||
border-radius: 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.06);
|
||||
animation: slideDown 0.3s ease-out;
|
||||
|
||||
&.dark {
|
||||
background: #292524;
|
||||
border-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
transform: translateY(-20rpx);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.banner-left {
|
||||
@@ -115,7 +132,7 @@ function handleJoin() {
|
||||
background: #10b981;
|
||||
border-radius: 50%;
|
||||
opacity: 0.2;
|
||||
animation: pulse 2s infinite;
|
||||
animation: pulse-ring 2s infinite;
|
||||
}
|
||||
|
||||
.icon-inner {
|
||||
@@ -123,47 +140,82 @@ function handleJoin() {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||
background: #10b981;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 8rpx 24rpx rgba(16, 185, 129, 0.2);
|
||||
overflow: hidden;
|
||||
|
||||
// 内部渐变光效
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(180deg, rgba(255,255,255,0.2) 0%, transparent 100%);
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-ring {
|
||||
0%, 100% { transform: scale(1); opacity: 0.2; }
|
||||
50% { transform: scale(1.15); opacity: 0.35; }
|
||||
}
|
||||
|
||||
.call-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.call-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
gap: 16rpx;
|
||||
|
||||
.title-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
letter-spacing: -0.5rpx;
|
||||
|
||||
.dark & { color: #f5f5f4; }
|
||||
}
|
||||
|
||||
.participant-count {
|
||||
padding: 4rpx 12rpx;
|
||||
background: rgba(16, 185, 129, 0.2);
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: #10b981;
|
||||
font-size: 20rpx;
|
||||
font-weight: 600;
|
||||
border-radius: 8rpx;
|
||||
font-family: monospace;
|
||||
border: 1rpx solid rgba(16, 185, 129, 0.15);
|
||||
|
||||
.dark & {
|
||||
background: rgba(16, 185, 129, 0.15);
|
||||
border-color: rgba(16, 185, 129, 0.25);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-stack {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 24rpx;
|
||||
|
||||
.stacked-avatar {
|
||||
border: 2rpx solid var(--bg-content);
|
||||
border: 4rpx solid #fff;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
|
||||
.dark & { border-color: #292524; }
|
||||
}
|
||||
|
||||
.more-count {
|
||||
@@ -171,19 +223,44 @@ function handleJoin() {
|
||||
height: 40rpx;
|
||||
margin-left: -16rpx;
|
||||
border-radius: 50%;
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-secondary);
|
||||
font-size: 18rpx;
|
||||
background: #f3f4f6;
|
||||
color: #6b7280;
|
||||
font-size: 16rpx;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 2rpx solid var(--bg-content);
|
||||
border: 4rpx solid #fff;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
|
||||
.dark & {
|
||||
background: #44403c;
|
||||
color: #a8a29e;
|
||||
border-color: #292524;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); opacity: 0.2; }
|
||||
50% { transform: scale(1.1); opacity: 0.4; }
|
||||
.join-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #10b981;
|
||||
padding: 16rpx 32rpx;
|
||||
border-radius: 24rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(16, 185, 129, 0.2);
|
||||
transition: all 0.15s;
|
||||
|
||||
text {
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
background: #059669;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,30 +1,33 @@
|
||||
<template>
|
||||
<view v-if="callState.incoming" class="incoming-call-overlay">
|
||||
<!-- 动态背景 -->
|
||||
<!-- 动态呼吸背景 -->
|
||||
<view class="bg-effects">
|
||||
<view class="glow glow-1"></view>
|
||||
<view class="glow glow-2"></view>
|
||||
</view>
|
||||
<view class="bg-overlay"></view>
|
||||
|
||||
<!-- 主要内容 -->
|
||||
<view class="content">
|
||||
<!-- 头像区域 -->
|
||||
<view class="avatar-section">
|
||||
<view class="avatar-ring ring-1"></view>
|
||||
<view class="avatar-ring ring-2"></view>
|
||||
<view class="avatar-ripple ripple-1"></view>
|
||||
<view class="avatar-ripple ripple-2"></view>
|
||||
<app-avatar
|
||||
:src="callState.inviterAvatar"
|
||||
:name="callState.inviterName"
|
||||
:size="200"
|
||||
radius="50%"
|
||||
:size="280"
|
||||
round
|
||||
class="main-avatar"
|
||||
/>
|
||||
<view class="call-type-badge">
|
||||
<wd-icon
|
||||
:name="callState.type === 'video' ? 'video' : 'phone'"
|
||||
size="32rpx"
|
||||
color="#fff"
|
||||
/>
|
||||
<svg v-if="callState.type === 'video'" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2">
|
||||
<polygon points="23 7 16 12 23 17 23 7"/>
|
||||
<rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>
|
||||
</svg>
|
||||
<svg v-else viewBox="0 0 24 24" fill="none" stroke="#fff" 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 12.84 12.84 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.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/>
|
||||
</svg>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -44,7 +47,11 @@
|
||||
<!-- 拒绝 -->
|
||||
<view class="action-item" @click="handleReject">
|
||||
<view class="action-btn reject-btn">
|
||||
<wd-icon name="close" size="48rpx" color="#fff" />
|
||||
<!-- phone-off icon -->
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2">
|
||||
<path d="M10.68 13.31a16 16 0 0 0 3.41 2.6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7 2 2 0 0 1 1.72 2v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.42 19.42 0 0 1-3.33-2.67m-2.67-3.34a19.79 19.79 0 0 1-3.07-8.63A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91"/>
|
||||
<line x1="1" y1="1" x2="23" y2="23"/>
|
||||
</svg>
|
||||
</view>
|
||||
<text class="action-label">拒绝</text>
|
||||
</view>
|
||||
@@ -52,11 +59,10 @@
|
||||
<!-- 接听 -->
|
||||
<view class="action-item" @click="handleAccept">
|
||||
<view class="action-btn accept-btn">
|
||||
<wd-icon
|
||||
:name="callState.type === 'video' ? 'video' : 'phone'"
|
||||
size="48rpx"
|
||||
color="#fff"
|
||||
/>
|
||||
<!-- phone icon filled style -->
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" stroke="none">
|
||||
<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.84 12.84 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.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/>
|
||||
</svg>
|
||||
</view>
|
||||
<text class="action-label">接听</text>
|
||||
</view>
|
||||
@@ -84,12 +90,9 @@ function handleReject() {
|
||||
<style lang="scss" scoped>
|
||||
.incoming-call-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
inset: 0;
|
||||
z-index: 10000;
|
||||
background: rgba(15, 23, 42, 0.95);
|
||||
background: #0f172a;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@@ -105,79 +108,99 @@ function handleReject() {
|
||||
|
||||
.glow {
|
||||
position: absolute;
|
||||
width: 600rpx;
|
||||
height: 600rpx;
|
||||
width: 1000rpx;
|
||||
height: 1000rpx;
|
||||
border-radius: 50%;
|
||||
filter: blur(100px);
|
||||
animation: pulse 6s ease-in-out infinite;
|
||||
filter: blur(160rpx);
|
||||
opacity: 0.4;
|
||||
animation: blob 8s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.glow-1 {
|
||||
top: 20%;
|
||||
left: 20%;
|
||||
background: rgba(139, 92, 246, 0.2);
|
||||
top: -20%;
|
||||
left: -20%;
|
||||
background: #8b5cf6;
|
||||
}
|
||||
|
||||
.glow-2 {
|
||||
bottom: 20%;
|
||||
right: 20%;
|
||||
background: rgba(59, 130, 246, 0.2);
|
||||
animation-delay: 1s;
|
||||
bottom: -20%;
|
||||
right: -20%;
|
||||
background: #3b82f6;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
}
|
||||
|
||||
.bg-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(15, 23, 42, 0.8);
|
||||
backdrop-filter: blur(120rpx);
|
||||
}
|
||||
|
||||
@keyframes blob {
|
||||
0%, 100% { transform: translate(0, 0) scale(1); }
|
||||
33% { transform: translate(60rpx, -100rpx) scale(1.1); }
|
||||
66% { transform: translate(-40rpx, 40rpx) scale(0.9); }
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
animation: slideUp 0.6s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
margin-top: -160rpx;
|
||||
animation: popIn 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
}
|
||||
|
||||
.avatar-section {
|
||||
position: relative;
|
||||
margin-bottom: 48rpx;
|
||||
margin-bottom: 64rpx;
|
||||
|
||||
.avatar-ring {
|
||||
.avatar-ripple {
|
||||
position: absolute;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.1);
|
||||
animation: ping 2s cubic-bezier(0, 0, 0.2, 1) infinite;
|
||||
inset: -20rpx;
|
||||
animation: ripple 2s infinite linear;
|
||||
|
||||
&.ring-1 {
|
||||
inset: -20rpx;
|
||||
}
|
||||
|
||||
&.ring-2 {
|
||||
inset: -40rpx;
|
||||
&.ripple-2 {
|
||||
animation-delay: 0.5s;
|
||||
}
|
||||
}
|
||||
|
||||
.main-avatar {
|
||||
border: 8rpx solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.3);
|
||||
border: 12rpx solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 40rpx 100rpx rgba(0, 0, 0, 0.4);
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.call-type-badge {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
bottom: 4rpx;
|
||||
right: 4rpx;
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||
background: #10b981;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 6rpx solid rgba(15, 23, 42, 0.95);
|
||||
box-shadow: 0 8rpx 20rpx rgba(16, 185, 129, 0.4);
|
||||
border: 8rpx solid #0f172a;
|
||||
box-shadow: 0 8rpx 24rpx rgba(16, 185, 129, 0.4);
|
||||
z-index: 20;
|
||||
|
||||
svg {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.inviter-name {
|
||||
font-size: 48rpx;
|
||||
font-size: 60rpx;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
margin-bottom: 16rpx;
|
||||
@@ -188,9 +211,9 @@ function handleReject() {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
font-size: 30rpx;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
margin-bottom: 80rpx;
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin-bottom: 128rpx;
|
||||
}
|
||||
|
||||
.typing-dots {
|
||||
@@ -200,66 +223,90 @@ function handleReject() {
|
||||
.dot {
|
||||
width: 8rpx;
|
||||
height: 8rpx;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
border-radius: 50%;
|
||||
animation: bounce 1.4s infinite;
|
||||
|
||||
&:nth-child(2) { animation-delay: 0.2s; }
|
||||
&:nth-child(3) { animation-delay: 0.4s; }
|
||||
&:nth-child(2) { animation-delay: 0.15s; }
|
||||
&:nth-child(3) { animation-delay: 0.3s; }
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 120rpx;
|
||||
gap: 128rpx;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
padding: 0 80rpx;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.action-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
svg {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
}
|
||||
|
||||
&:active { transform: scale(0.95); }
|
||||
}
|
||||
|
||||
.reject-btn {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
border: 2rpx solid rgba(239, 68, 68, 0.3);
|
||||
width: 128rpx;
|
||||
height: 128rpx;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(8px);
|
||||
|
||||
&:active {
|
||||
background: #ef4444;
|
||||
&:hover {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: #ef4444;
|
||||
}
|
||||
}
|
||||
|
||||
.accept-btn {
|
||||
width: 140rpx;
|
||||
height: 140rpx;
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
background: #10b981;
|
||||
box-shadow: 0 10rpx 40rpx rgba(16, 185, 129, 0.4);
|
||||
animation: pulse-glow 2s infinite;
|
||||
box-shadow: 0 0 80rpx rgba(16, 185, 129, 0.4);
|
||||
color: #fff;
|
||||
|
||||
&:active {
|
||||
background: #059669;
|
||||
svg {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #10b981;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: #059669;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
.action-label {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
@@ -267,29 +314,23 @@ function handleReject() {
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from { transform: translateY(40rpx); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
@keyframes popIn {
|
||||
0% { transform: scale(0.8); opacity: 0; }
|
||||
100% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes ping {
|
||||
0% { transform: scale(1); opacity: 0.3; }
|
||||
75%, 100% { transform: scale(1.3); opacity: 0; }
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 0.2; }
|
||||
50% { opacity: 0.5; }
|
||||
@keyframes ripple {
|
||||
0% { transform: scale(1); opacity: 0.4; }
|
||||
100% { transform: scale(2.5); opacity: 0; }
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 80%, 100% { transform: translateY(0); }
|
||||
40% { transform: translateY(-10rpx); }
|
||||
40% { transform: translateY(-8rpx); }
|
||||
}
|
||||
|
||||
@keyframes pulse-glow {
|
||||
0%, 100% { box-shadow: 0 10rpx 40rpx rgba(16, 185, 129, 0.4); }
|
||||
50% { box-shadow: 0 10rpx 60rpx rgba(16, 185, 129, 0.6); }
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,47 +1,46 @@
|
||||
<template>
|
||||
<wd-popup
|
||||
v-model="modelValue"
|
||||
position="bottom"
|
||||
:safe-area-inset-bottom="true"
|
||||
custom-style="border-radius: 24rpx 24rpx 0 0; max-height: 80vh;"
|
||||
@close="handleClose"
|
||||
>
|
||||
<view class="call-modal" :class="{ dark: isDark }">
|
||||
<view v-if="modelValue" class="modal-container" :class="{ dark: isDark }">
|
||||
<!-- 遮罩 -->
|
||||
<view
|
||||
class="modal-mask"
|
||||
:class="{ 'mask-active': showContent }"
|
||||
@click="handleClose"
|
||||
/>
|
||||
|
||||
<!-- 弹窗内容 -->
|
||||
<view class="call-modal" :class="{ 'modal-active': showContent }">
|
||||
<!-- 头部 -->
|
||||
<view class="modal-header">
|
||||
<view class="header-info">
|
||||
<text class="header-title">发起群通话</text>
|
||||
<text class="header-subtitle">选择成员加入通话 ({{ selectedIds.size }}/{{ members.length }})</text>
|
||||
<text class="header-subtitle">选择成员加入通话</text>
|
||||
</view>
|
||||
<view class="close-btn" @click="handleClose">
|
||||
<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"/>
|
||||
</svg>
|
||||
</view>
|
||||
<wd-icon
|
||||
name="close"
|
||||
size="40rpx"
|
||||
color="var(--text-secondary)"
|
||||
@click="handleClose"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 搜索与全选 -->
|
||||
<!-- 搜索 -->
|
||||
<view class="search-bar">
|
||||
<wd-search
|
||||
v-model="searchQuery"
|
||||
placeholder="搜索成员..."
|
||||
hide-cancel
|
||||
/>
|
||||
<wd-button
|
||||
size="small"
|
||||
:type="isAllSelected ? 'success' : 'default'"
|
||||
plain
|
||||
@click="toggleSelectAll"
|
||||
>
|
||||
{{ isAllSelected ? '取消全选' : '全选' }}
|
||||
</wd-button>
|
||||
<view class="search-input">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="11" cy="11" r="8"/>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
||||
</svg>
|
||||
<input type="text" v-model="searchQuery" placeholder="搜索成员" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 成员列表 -->
|
||||
<scroll-view class="member-list" scroll-y>
|
||||
<view v-if="filteredMembers.length === 0" class="empty-state">
|
||||
<wd-icon name="search" size="80rpx" color="var(--text-tertiary)" />
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<circle cx="11" cy="11" r="8"/>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
||||
</svg>
|
||||
<text class="empty-text">未找到成员</text>
|
||||
</view>
|
||||
|
||||
@@ -52,21 +51,21 @@
|
||||
class="member-item"
|
||||
@click="toggleSelection(member.id || member.user_id || '')"
|
||||
>
|
||||
<!-- 头像 -->
|
||||
<view class="avatar-wrap">
|
||||
<view class="avatar-wrap" :class="{ selected: selectedIds.has(member.id || member.user_id || '') }">
|
||||
<app-avatar
|
||||
:src="member.user?.avatar"
|
||||
:name="member.remark_name || member.user?.name || '?'"
|
||||
:size="100"
|
||||
radius="50%"
|
||||
:class="{ 'avatar-selected': selectedIds.has(member.id || member.user_id || '') }"
|
||||
:size="112"
|
||||
round
|
||||
/>
|
||||
<!-- 选中标记 -->
|
||||
<view
|
||||
v-if="selectedIds.has(member.id || member.user_id || '')"
|
||||
class="check-badge"
|
||||
>
|
||||
<wd-icon name="check" size="20rpx" color="#fff" />
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="3">
|
||||
<polyline points="20 6 9 17 4 12"/>
|
||||
</svg>
|
||||
</view>
|
||||
<!-- 在线状态 -->
|
||||
<view
|
||||
@@ -74,7 +73,6 @@
|
||||
class="online-dot"
|
||||
></view>
|
||||
</view>
|
||||
<!-- 名字 -->
|
||||
<text
|
||||
class="member-name"
|
||||
:class="{ 'name-selected': selectedIds.has(member.id || member.user_id || '') }"
|
||||
@@ -87,34 +85,32 @@
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<view class="modal-footer">
|
||||
<text class="selected-count">
|
||||
已选择 <text class="count-num">{{ selectedIds.size }}</text> 人
|
||||
</text>
|
||||
<view class="selected-info">
|
||||
<text>已选择</text>
|
||||
<text class="count-num">{{ selectedIds.size }}</text>
|
||||
<text>人</text>
|
||||
</view>
|
||||
<view class="action-buttons">
|
||||
<wd-button
|
||||
:disabled="selectedIds.size === 0"
|
||||
plain
|
||||
@click="emitStartCall('audio')"
|
||||
>
|
||||
<wd-icon name="phone" size="32rpx" />
|
||||
<text style="margin-left: 8rpx;">语音</text>
|
||||
</wd-button>
|
||||
<wd-button
|
||||
type="success"
|
||||
:disabled="selectedIds.size === 0"
|
||||
@click="emitStartCall('video')"
|
||||
>
|
||||
<wd-icon name="video" size="32rpx" />
|
||||
<text style="margin-left: 8rpx;">视频</text>
|
||||
</wd-button>
|
||||
<view class="action-btn audio-btn" :class="{ disabled: selectedIds.size === 0 }" @click="emitStartCall('audio')">
|
||||
<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 12.84 12.84 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.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/>
|
||||
</svg>
|
||||
</view>
|
||||
<view class="action-btn video-btn" :class="{ disabled: selectedIds.size === 0 }" @click="emitStartCall('video')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2">
|
||||
<polygon points="23 7 16 12 23 17 23 7"/>
|
||||
<rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>
|
||||
</svg>
|
||||
<text>开始</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { ref, computed, watch, nextTick } from 'vue'
|
||||
import { useTheme } from '@/composables/useTheme'
|
||||
import AppAvatar from '@/components/common/AppAvatar.vue'
|
||||
import type { Contact } from '@/types/api'
|
||||
@@ -132,16 +128,20 @@ const emit = defineEmits<{
|
||||
const { isDark } = useTheme()
|
||||
const searchQuery = ref('')
|
||||
const selectedIds = ref<Set<string>>(new Set())
|
||||
const showContent = ref(false)
|
||||
|
||||
// 监听打开/关闭重置状态
|
||||
watch(() => props.modelValue, (val) => {
|
||||
if (val) {
|
||||
searchQuery.value = ''
|
||||
selectedIds.value = new Set()
|
||||
nextTick(() => {
|
||||
showContent.value = true
|
||||
})
|
||||
} else {
|
||||
showContent.value = false
|
||||
}
|
||||
})
|
||||
|
||||
// 过滤后的成员
|
||||
const filteredMembers = computed(() => {
|
||||
const query = searchQuery.value.trim().toLowerCase()
|
||||
if (!query) return props.members
|
||||
@@ -151,10 +151,6 @@ const filteredMembers = computed(() => {
|
||||
})
|
||||
})
|
||||
|
||||
const isAllSelected = computed(() => {
|
||||
return filteredMembers.value.length > 0 && selectedIds.value.size === filteredMembers.value.length
|
||||
})
|
||||
|
||||
function toggleSelection(id: string) {
|
||||
if (!id) return
|
||||
if (selectedIds.value.has(id)) {
|
||||
@@ -162,19 +158,9 @@ function toggleSelection(id: string) {
|
||||
} else {
|
||||
selectedIds.value.add(id)
|
||||
}
|
||||
// 触发响应式更新
|
||||
selectedIds.value = new Set(selectedIds.value)
|
||||
}
|
||||
|
||||
function toggleSelectAll() {
|
||||
if (isAllSelected.value) {
|
||||
selectedIds.value = new Set()
|
||||
} else {
|
||||
const ids = filteredMembers.value.map(m => m.id || m.user_id || '').filter(Boolean)
|
||||
selectedIds.value = new Set(ids)
|
||||
}
|
||||
}
|
||||
|
||||
function emitStartCall(type: 'audio' | 'video') {
|
||||
if (selectedIds.value.size === 0) return
|
||||
emit('start-call', type, Array.from(selectedIds.value))
|
||||
@@ -182,21 +168,73 @@ function emitStartCall(type: 'audio' | 'video') {
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
emit('update:modelValue', false)
|
||||
showContent.value = false
|
||||
setTimeout(() => {
|
||||
emit('update:modelValue', false)
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.modal-container {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1000;
|
||||
|
||||
--bg-surface: #fff;
|
||||
--bg-hover: #f5f5f5;
|
||||
--text-primary: #1f2937;
|
||||
--text-secondary: #6b7280;
|
||||
--text-tertiary: #9ca3af;
|
||||
--border-color: rgba(0, 0, 0, 0.05);
|
||||
--color-brand: #4F46E5;
|
||||
|
||||
&.dark {
|
||||
--bg-surface: #1c1917;
|
||||
--bg-hover: #292524;
|
||||
--text-primary: #f5f5f4;
|
||||
--text-secondary: #a8a29e;
|
||||
--text-tertiary: #78716c;
|
||||
--border-color: rgba(255, 255, 255, 0.08);
|
||||
--color-brand: #4F46E5;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-mask {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
backdrop-filter: blur(4rpx);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
|
||||
&.mask-active {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.call-modal {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg-content);
|
||||
max-height: 80vh;
|
||||
background: var(--bg-surface);
|
||||
max-height: 85vh;
|
||||
border-radius: 64rpx 64rpx 0 0;
|
||||
box-shadow: 0 -20rpx 80rpx rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(100%);
|
||||
transition: transform 0.3s cubic-bezier(0.32, 0.72, 0, 1);
|
||||
|
||||
&.modal-active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 32rpx;
|
||||
border-bottom: 1rpx solid var(--divider-color);
|
||||
padding: 40rpx 40rpx 24rpx;
|
||||
border-bottom: 1rpx solid var(--border-color);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
@@ -209,9 +247,10 @@ function handleClose() {
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
letter-spacing: -0.5rpx;
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
@@ -219,18 +258,69 @@ function handleClose() {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
background: var(--bg-hover);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.15s;
|
||||
|
||||
svg {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
background: var(--border-color);
|
||||
}
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
padding: 24rpx 32rpx;
|
||||
padding: 24rpx 40rpx;
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
|
||||
.dark & {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
}
|
||||
|
||||
.search-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
background: var(--bg-hover);
|
||||
padding: 0 24rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 24rpx;
|
||||
|
||||
svg {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
color: var(--text-tertiary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
font-size: 28rpx;
|
||||
color: var(--text-primary);
|
||||
background: transparent;
|
||||
border: none;
|
||||
|
||||
&::placeholder { color: var(--text-tertiary); }
|
||||
}
|
||||
}
|
||||
|
||||
.member-list {
|
||||
flex: 1;
|
||||
max-height: 50vh;
|
||||
padding: 24rpx;
|
||||
padding: 32rpx 40rpx 160rpx;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
@@ -238,37 +328,46 @@ function handleClose() {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80rpx 0;
|
||||
gap: 16rpx;
|
||||
padding: 100rpx 0;
|
||||
gap: 24rpx;
|
||||
|
||||
svg {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
color: var(--text-tertiary);
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 26rpx;
|
||||
font-size: 28rpx;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
.member-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 32rpx;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 48rpx 16rpx;
|
||||
}
|
||||
|
||||
.member-item {
|
||||
width: calc(20% - 26rpx);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.avatar-wrap {
|
||||
position: relative;
|
||||
transition: all 0.2s;
|
||||
|
||||
.avatar-selected {
|
||||
opacity: 1;
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 0 0 4rpx #10b981;
|
||||
border-radius: 50%;
|
||||
&.selected {
|
||||
transform: scale(0.95);
|
||||
|
||||
:deep(.app-avatar) {
|
||||
box-shadow: 0 0 0 8rpx var(--color-brand);
|
||||
border-radius: 32rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,25 +375,31 @@ function handleClose() {
|
||||
position: absolute;
|
||||
top: -8rpx;
|
||||
right: -8rpx;
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
background: #10b981;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
background: var(--color-brand);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4rpx 10rpx rgba(16, 185, 129, 0.4);
|
||||
border: 4rpx solid var(--bg-surface);
|
||||
box-shadow: 0 4rpx 12rpx rgba(79, 70, 229, 0.4);
|
||||
animation: bounceIn 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
|
||||
svg {
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.online-dot {
|
||||
position: absolute;
|
||||
bottom: 4rpx;
|
||||
right: 4rpx;
|
||||
width: 20rpx;
|
||||
height: 20rpx;
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
background: #10b981;
|
||||
border: 4rpx solid var(--bg-content);
|
||||
border: 4rpx solid var(--bg-surface);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
@@ -302,40 +407,98 @@ function handleClose() {
|
||||
font-size: 22rpx;
|
||||
color: var(--text-secondary);
|
||||
text-align: center;
|
||||
max-width: 100%;
|
||||
max-width: 128rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
transition: color 0.2s;
|
||||
font-weight: 500;
|
||||
|
||||
&.name-selected {
|
||||
color: #10b981;
|
||||
font-weight: 500;
|
||||
color: var(--color-brand);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 24rpx 32rpx;
|
||||
border-top: 1rpx solid var(--divider-color);
|
||||
padding-bottom: calc(64rpx + env(safe-area-inset-bottom));
|
||||
border-top: 1rpx solid var(--border-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: var(--bg-content);
|
||||
background: var(--bg-surface);
|
||||
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.02);
|
||||
border-radius: 32rpx 32rpx 0 0;
|
||||
}
|
||||
|
||||
.selected-count {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-tertiary);
|
||||
.selected-info {
|
||||
font-size: 28rpx;
|
||||
color: var(--text-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4rpx;
|
||||
|
||||
.count-num {
|
||||
color: #10b981;
|
||||
font-weight: 600;
|
||||
font-size: 28rpx;
|
||||
color: var(--color-brand);
|
||||
font-weight: 700;
|
||||
font-size: 36rpx;
|
||||
margin: 0 8rpx;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
border-radius: 32rpx;
|
||||
transition: all 0.15s;
|
||||
|
||||
svg {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.4;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
.audio-btn {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
background: var(--bg-hover);
|
||||
border: 1rpx solid var(--border-color);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.video-btn {
|
||||
height: 96rpx;
|
||||
padding: 0 48rpx;
|
||||
background: var(--color-brand);
|
||||
box-shadow: 0 8rpx 24rpx rgba(79, 70, 229, 0.3);
|
||||
|
||||
text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounceIn {
|
||||
@@ -343,4 +506,3 @@ function handleClose() {
|
||||
100% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -4,153 +4,154 @@
|
||||
class="call-window"
|
||||
:class="{ minimized: callState.minimized }"
|
||||
>
|
||||
<!-- 顶部功能栏 -->
|
||||
<view class="window-header" @click="handleMinimize">
|
||||
<!-- 最小化内容 -->
|
||||
<view v-if="callState.minimized" class="minimized-content" @click="toggleMinimize">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#10b981" stroke-width="2">
|
||||
<polygon points="23 7 16 12 23 17 23 7"/>
|
||||
<rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>
|
||||
</svg>
|
||||
<text class="mini-duration">{{ formatDuration(callState.duration) }}</text>
|
||||
<view class="mini-pulse"></view>
|
||||
</view>
|
||||
|
||||
<!-- 顶部功能栏 (渐变遮罩) -->
|
||||
<view v-if="!callState.minimized" class="window-header">
|
||||
<view class="header-left">
|
||||
<view class="status-dot"></view>
|
||||
<text class="header-title">群聊通话</text>
|
||||
<text class="duration">{{ formatDuration(callState.duration) }}</text>
|
||||
</view>
|
||||
<view class="header-right" @click.stop>
|
||||
<view class="minimize-btn" @click="toggleMinimize">
|
||||
<wd-icon :name="callState.minimized ? 'expand' : 'collapse'" size="32rpx" color="#fff" />
|
||||
</view>
|
||||
<view class="minimize-btn" @click="toggleMinimize">
|
||||
<!-- minimize-2 icon -->
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2">
|
||||
<polyline points="4 14 10 14 10 20"/>
|
||||
<polyline points="20 10 14 10 14 4"/>
|
||||
<line x1="14" y1="10" x2="21" y2="3"/>
|
||||
<line x1="3" y1="21" x2="10" y2="14"/>
|
||||
</svg>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最小化模式 -->
|
||||
<view v-if="callState.minimized" class="minimized-content" @click="toggleMinimize">
|
||||
<wd-icon name="phone" size="48rpx" color="#10b981" />
|
||||
<text class="mini-duration">{{ formatDuration(callState.duration) }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 正常模式 -->
|
||||
<template v-else>
|
||||
<!-- 视频网格 -->
|
||||
<scroll-view class="video-grid" scroll-y>
|
||||
<!-- 自己的画面 -->
|
||||
<view class="video-item self-video">
|
||||
<!-- #ifdef H5 -->
|
||||
<video
|
||||
v-if="localStream && !callState.isSelfCamOff"
|
||||
ref="localVideoRef"
|
||||
class="video-element mirror"
|
||||
autoplay
|
||||
muted
|
||||
playsinline
|
||||
:srcObject="localStream"
|
||||
></video>
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- 摄像头关闭时显示头像 -->
|
||||
<view v-if="callState.isSelfCamOff" class="cam-off-placeholder">
|
||||
<app-avatar
|
||||
:name="'我'"
|
||||
:size="120"
|
||||
radius="50%"
|
||||
/>
|
||||
<text class="cam-off-text">摄像头已关闭</text>
|
||||
</view>
|
||||
|
||||
<view class="video-label">
|
||||
<text class="label-name">我</text>
|
||||
<wd-icon
|
||||
v-if="callState.isSelfMuted"
|
||||
name="volume-mute"
|
||||
size="24rpx"
|
||||
color="#ef4444"
|
||||
/>
|
||||
</view>
|
||||
<!-- 视频网格 -->
|
||||
<scroll-view v-if="!callState.minimized" class="video-grid" scroll-y>
|
||||
<!-- 自己的画面 -->
|
||||
<view class="video-item self-video">
|
||||
<!-- #ifdef H5 -->
|
||||
<video
|
||||
v-if="localStream && !callState.isSelfCamOff"
|
||||
ref="localVideoRef"
|
||||
class="video-element mirror"
|
||||
autoplay
|
||||
muted
|
||||
playsinline
|
||||
:srcObject="localStream"
|
||||
></video>
|
||||
<!-- #endif -->
|
||||
|
||||
<view v-if="callState.isSelfCamOff" class="cam-off-placeholder">
|
||||
<app-avatar :name="'我'" :size="160" round />
|
||||
<text class="cam-off-text">摄像头已关闭</text>
|
||||
</view>
|
||||
|
||||
<!-- 其他成员 -->
|
||||
<view
|
||||
v-for="user in participants"
|
||||
:key="user.userId"
|
||||
class="video-item"
|
||||
>
|
||||
<!-- #ifdef H5 -->
|
||||
<video
|
||||
v-if="user.stream && !user.isCamOff"
|
||||
class="video-element"
|
||||
autoplay
|
||||
playsinline
|
||||
:srcObject="user.stream"
|
||||
></video>
|
||||
<!-- #endif -->
|
||||
<view class="video-label">
|
||||
<text class="label-name">我</text>
|
||||
<svg v-if="callState.isSelfMuted" class="mute-icon" viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="2">
|
||||
<line x1="1" y1="1" x2="23" y2="23"/>
|
||||
<path d="M9 9v3a3 3 0 0 0 5.12 2.12M15 9.34V4a3 3 0 0 0-5.94-.6"/>
|
||||
</svg>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 无视频流或摄像头关闭 -->
|
||||
<view v-if="!user.stream || user.isCamOff" class="cam-off-placeholder">
|
||||
<view class="avatar-wrap">
|
||||
<app-avatar
|
||||
:src="user.avatar"
|
||||
:name="user.name"
|
||||
:size="120"
|
||||
radius="50%"
|
||||
/>
|
||||
<view v-if="user.status === 'connecting'" class="connecting-overlay">
|
||||
<wd-loading color="#fff" size="32rpx" />
|
||||
</view>
|
||||
<!-- 其他成员 -->
|
||||
<view
|
||||
v-for="user in participants"
|
||||
:key="user.userId"
|
||||
class="video-item"
|
||||
>
|
||||
<!-- #ifdef H5 -->
|
||||
<video
|
||||
v-if="user.stream && !user.isCamOff"
|
||||
class="video-element"
|
||||
autoplay
|
||||
playsinline
|
||||
:srcObject="user.stream"
|
||||
></video>
|
||||
<!-- #endif -->
|
||||
|
||||
<view v-if="!user.stream || user.isCamOff" class="cam-off-placeholder">
|
||||
<view class="avatar-wrap">
|
||||
<app-avatar :src="user.avatar" :name="user.name" :size="160" round />
|
||||
<view v-if="user.status === 'connecting'" class="connecting-overlay">
|
||||
<wd-loading color="#fff" size="32rpx" />
|
||||
</view>
|
||||
<text class="participant-name">{{ user.name }}</text>
|
||||
<text class="participant-status">
|
||||
{{ user.status === 'connecting' ? '连接中...' : user.status === 'rejected' ? '已拒绝' : '摄像头关闭' }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="video-label">
|
||||
<text class="label-name">{{ user.name }}</text>
|
||||
<wd-icon
|
||||
v-if="user.isMuted"
|
||||
name="volume-mute"
|
||||
size="24rpx"
|
||||
color="#ef4444"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 说话指示 -->
|
||||
<view
|
||||
v-if="!user.isMuted && user.volume && user.volume > 0.05"
|
||||
class="speaking-indicator"
|
||||
></view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部控制栏 -->
|
||||
<view class="control-bar">
|
||||
<!-- 静音 -->
|
||||
<view class="control-item" @click="webRTC.toggleSelfMute()">
|
||||
<view class="control-btn" :class="{ active: callState.isSelfMuted }">
|
||||
<wd-icon
|
||||
:name="callState.isSelfMuted ? 'volume-mute' : 'volume'"
|
||||
size="40rpx"
|
||||
color="#fff"
|
||||
/>
|
||||
</view>
|
||||
<text class="control-label">{{ callState.isSelfMuted ? '已静音' : '静音' }}</text>
|
||||
<text class="participant-name">{{ user.name }}</text>
|
||||
<text class="participant-status">
|
||||
{{ user.status === 'connecting' ? '连接中...' : user.status === 'rejected' ? '已拒绝' : '摄像头关闭' }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 挂断 -->
|
||||
<view class="control-item" @click="webRTC.leaveCall()">
|
||||
<view class="control-btn hangup-btn">
|
||||
<wd-icon name="phone-off" size="48rpx" color="#fff" />
|
||||
</view>
|
||||
<text class="control-label">挂断</text>
|
||||
<view class="video-label">
|
||||
<text class="label-name">{{ user.name }}</text>
|
||||
<svg v-if="user.isMuted" class="mute-icon" viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="2">
|
||||
<line x1="1" y1="1" x2="23" y2="23"/>
|
||||
<path d="M9 9v3a3 3 0 0 0 5.12 2.12M15 9.34V4a3 3 0 0 0-5.94-.6"/>
|
||||
</svg>
|
||||
</view>
|
||||
|
||||
<!-- 摄像头 -->
|
||||
<view class="control-item" @click="webRTC.toggleSelfCamera()">
|
||||
<view class="control-btn" :class="{ active: callState.isSelfCamOff }">
|
||||
<wd-icon
|
||||
:name="callState.isSelfCamOff ? 'video-off' : 'video'"
|
||||
size="40rpx"
|
||||
color="#fff"
|
||||
/>
|
||||
</view>
|
||||
<text class="control-label">{{ callState.isSelfCamOff ? '已关闭' : '摄像头' }}</text>
|
||||
</view>
|
||||
<!-- 说话指示 -->
|
||||
<view
|
||||
v-if="!user.isMuted && user.volume && user.volume > 0.05"
|
||||
class="speaking-indicator"
|
||||
></view>
|
||||
</view>
|
||||
</template>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部控制栏 -->
|
||||
<view v-if="!callState.minimized" class="control-bar">
|
||||
<view class="control-item" @click="webRTC.toggleSelfMute()">
|
||||
<view class="control-btn" :class="{ active: callState.isSelfMuted }">
|
||||
<svg v-if="callState.isSelfMuted" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="1" y1="1" x2="23" y2="23"/>
|
||||
<path d="M9 9v3a3 3 0 0 0 5.12 2.12M15 9.34V4a3 3 0 0 0-5.94-.6"/>
|
||||
<path d="M17 16.95A7 7 0 0 1 5 12v-2m14 0v2a7 7 0 0 1-.11 1.23"/>
|
||||
<line x1="12" y1="19" x2="12" y2="23"/>
|
||||
<line x1="8" y1="23" x2="16" y2="23"/>
|
||||
</svg>
|
||||
<svg v-else viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<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>
|
||||
<text class="control-label">{{ callState.isSelfMuted ? '已静音' : '静音' }}</text>
|
||||
</view>
|
||||
|
||||
<view class="control-item" @click="webRTC.leaveCall()">
|
||||
<view class="control-btn hangup-btn">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.5">
|
||||
<path d="M10.68 13.31a16 16 0 0 0 3.41 2.6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7 2 2 0 0 1 1.72 2v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.42 19.42 0 0 1-3.33-2.67m-2.67-3.34a19.79 19.79 0 0 1-3.07-8.63A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91"/>
|
||||
<line x1="1" y1="1" x2="23" y2="23"/>
|
||||
</svg>
|
||||
</view>
|
||||
<text class="control-label">挂断</text>
|
||||
</view>
|
||||
|
||||
<view class="control-item" @click="webRTC.toggleSelfCamera()">
|
||||
<view class="control-btn" :class="{ active: callState.isSelfCamOff }">
|
||||
<svg v-if="callState.isSelfCamOff" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M16 16v1a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h2m5.66 0H14a2 2 0 0 1 2 2v3.34l1 1L23 7v10"/>
|
||||
<line x1="1" y1="1" x2="23" y2="23"/>
|
||||
</svg>
|
||||
<svg v-else viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polygon points="23 7 16 12 23 17 23 7"/>
|
||||
<rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>
|
||||
</svg>
|
||||
</view>
|
||||
<text class="control-label">{{ callState.isSelfCamOff ? '已关闭' : '摄像头' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -164,7 +165,6 @@ const { callState, participants, formatDuration, localStream } = webRTC
|
||||
|
||||
const localVideoRef = ref<HTMLVideoElement | null>(null)
|
||||
|
||||
// 绑定本地视频流
|
||||
watchEffect(() => {
|
||||
const stream = localStream.value
|
||||
const videoEl = localVideoRef.value
|
||||
@@ -180,69 +180,92 @@ watchEffect(() => {
|
||||
function toggleMinimize() {
|
||||
callState.minimized = !callState.minimized
|
||||
}
|
||||
|
||||
function handleMinimize() {
|
||||
if (callState.minimized) {
|
||||
toggleMinimize()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.call-window {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
background: #1e1e1e;
|
||||
background: #1c1c1e;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.5);
|
||||
transition: all 0.3s ease;
|
||||
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
|
||||
// 正常模式
|
||||
&:not(.minimized) {
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border-radius: 0;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
// 最小化模式
|
||||
&.minimized {
|
||||
top: calc(var(--status-bar-height, 0) + 160rpx);
|
||||
right: 24rpx;
|
||||
top: calc(var(--status-bar-height, 0) + 240rpx);
|
||||
right: 32rpx;
|
||||
width: 200rpx;
|
||||
height: 280rpx;
|
||||
border-radius: 24rpx;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 32rpx;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.minimized-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16rpx;
|
||||
background: #2c2c2e;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
svg {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
}
|
||||
|
||||
.mini-duration {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.mini-pulse {
|
||||
position: absolute;
|
||||
bottom: 16rpx;
|
||||
width: 8rpx;
|
||||
height: 8rpx;
|
||||
background: #10b981;
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
.window-header {
|
||||
height: 88rpx;
|
||||
padding: 0 24rpx;
|
||||
height: 200rpx;
|
||||
padding: 0 40rpx;
|
||||
padding-top: calc(24rpx + var(--status-bar-height, 0));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: linear-gradient(180deg, rgba(0, 0, 0, 0.8) 0%, transparent 100%);
|
||||
background: linear-gradient(180deg, rgba(0, 0, 0, 0.6) 0%, transparent 100%);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 50;
|
||||
|
||||
.minimized & {
|
||||
background: transparent;
|
||||
height: 60rpx;
|
||||
padding: 0 16rpx;
|
||||
}
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
gap: 24rpx;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
@@ -250,77 +273,64 @@ function handleMinimize() {
|
||||
height: 16rpx;
|
||||
border-radius: 50%;
|
||||
background: #10b981;
|
||||
box-shadow: 0 0 20rpx #10b981;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
|
||||
.minimized & {
|
||||
display: none;
|
||||
}
|
||||
letter-spacing: 0.5rpx;
|
||||
}
|
||||
|
||||
.duration {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
font-family: monospace;
|
||||
padding-left: 12rpx;
|
||||
border-left: 2rpx solid rgba(255, 255, 255, 0.2);
|
||||
margin-left: 4rpx;
|
||||
|
||||
.minimized & {
|
||||
display: none;
|
||||
}
|
||||
padding-left: 24rpx;
|
||||
border-left: 2rpx solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.minimize-btn {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(8px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.15s;
|
||||
pointer-events: auto;
|
||||
|
||||
svg {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.9);
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.minimized-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16rpx;
|
||||
background: #252525;
|
||||
}
|
||||
|
||||
.mini-duration {
|
||||
font-size: 20rpx;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.video-grid {
|
||||
flex: 1;
|
||||
padding: 100rpx 16rpx 16rpx;
|
||||
padding: calc(var(--status-bar-height, 0) + 200rpx) 24rpx 240rpx;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
align-content: flex-start;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.video-item {
|
||||
position: relative;
|
||||
width: calc(50% - 8rpx);
|
||||
aspect-ratio: 4 / 3;
|
||||
background: #252525;
|
||||
border-radius: 16rpx;
|
||||
aspect-ratio: 3 / 4;
|
||||
background: #2c2c2e;
|
||||
border-radius: 32rpx;
|
||||
overflow: hidden;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
@@ -330,9 +340,7 @@ function handleMinimize() {
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
|
||||
&.mirror {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
&.mirror { transform: scaleX(-1); }
|
||||
}
|
||||
|
||||
.cam-off-placeholder {
|
||||
@@ -343,7 +351,7 @@ function handleMinimize() {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16rpx;
|
||||
background: #252525;
|
||||
background: #2c2c2e;
|
||||
}
|
||||
|
||||
.avatar-wrap {
|
||||
@@ -360,104 +368,125 @@ function handleMinimize() {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cam-off-text,
|
||||
.participant-name {
|
||||
font-size: 24rpx;
|
||||
.cam-off-text, .participant-name {
|
||||
font-size: 26rpx;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.participant-status {
|
||||
font-size: 20rpx;
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.video-label {
|
||||
position: absolute;
|
||||
bottom: 12rpx;
|
||||
left: 12rpx;
|
||||
bottom: 24rpx;
|
||||
left: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
gap: 12rpx;
|
||||
padding: 8rpx 16rpx;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 8rpx;
|
||||
backdrop-filter: blur(4px);
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
border-radius: 16rpx;
|
||||
backdrop-filter: blur(16rpx);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.1);
|
||||
|
||||
.label-name {
|
||||
font-size: 22rpx;
|
||||
font-size: 20rpx;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
max-width: 120rpx;
|
||||
max-width: 140rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mute-icon {
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.speaking-indicator {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border: 4rpx solid rgba(16, 185, 129, 0.6);
|
||||
border-radius: 16rpx;
|
||||
border: 4rpx solid #10b981;
|
||||
border-radius: 32rpx;
|
||||
pointer-events: none;
|
||||
animation: speakPulse 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
.control-bar {
|
||||
height: 180rpx;
|
||||
padding: 24rpx 0 40rpx;
|
||||
height: 320rpx;
|
||||
padding: 40rpx 0;
|
||||
padding-bottom: calc(64rpx + env(safe-area-inset-bottom));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 80rpx;
|
||||
background: linear-gradient(0deg, rgba(0, 0, 0, 0.8) 0%, transparent 100%);
|
||||
background: linear-gradient(0deg, rgba(0, 0, 0, 0.9) 0%, rgba(0, 0, 0, 0.5) 50%, transparent 100%);
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.control-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
width: 112rpx;
|
||||
height: 112rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.05);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
transition: all 0.3s;
|
||||
backdrop-filter: blur(8rpx);
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
svg {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
}
|
||||
|
||||
&:active { transform: scale(0.95); }
|
||||
|
||||
&.active {
|
||||
background: #fff;
|
||||
|
||||
:deep(.wd-icon) {
|
||||
color: #000 !important;
|
||||
}
|
||||
color: #000;
|
||||
border-color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.hangup-btn {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 32rpx;
|
||||
width: 144rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 56rpx;
|
||||
background: #ef4444;
|
||||
box-shadow: 0 10rpx 40rpx rgba(239, 68, 68, 0.4);
|
||||
border: none;
|
||||
box-shadow: 0 8rpx 32rpx rgba(239, 68, 68, 0.3);
|
||||
padding: 0 32rpx;
|
||||
|
||||
&:active {
|
||||
background: #dc2626;
|
||||
svg {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
}
|
||||
|
||||
&:active { background: #dc2626; }
|
||||
}
|
||||
|
||||
.control-label {
|
||||
font-size: 20rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
@@ -471,4 +500,3 @@ function handleMinimize() {
|
||||
100% { opacity: 0; }
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -112,3 +112,9 @@ const bubbleClass = computed(() => {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
<template>
|
||||
<view class="message-input">
|
||||
<!-- @ 提及弹窗 -->
|
||||
<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>
|
||||
<wd-icon name="close" size="40rpx" @click="showMentionPicker = false" />
|
||||
<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"/>
|
||||
</svg>
|
||||
</view>
|
||||
</view>
|
||||
<scroll-view scroll-y class="mention-list">
|
||||
<view
|
||||
@@ -26,49 +31,96 @@
|
||||
</view>
|
||||
</wd-popup>
|
||||
|
||||
<!-- 工具栏 -->
|
||||
<view class="input-toolbar">
|
||||
<wd-icon name="emotion" size="48rpx" class="tool-icon" @click="$emit('emoji')" />
|
||||
<wd-icon name="picture" size="48rpx" class="tool-icon" @click="$emit('image')" />
|
||||
<wd-icon name="folder" size="48rpx" class="tool-icon" @click="$emit('file')" />
|
||||
<wd-icon v-if="isGroup" name="at-sign" size="48rpx" class="tool-icon" @click="openMentionPicker" />
|
||||
<!-- 录音浮层 -->
|
||||
<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>
|
||||
</view>
|
||||
<view class="wave-bars">
|
||||
<view class="wave-bar" v-for="i in 5" :key="i" :style="{ animationDelay: `${i * 0.1}s` }"></view>
|
||||
</view>
|
||||
<text class="recording-duration">{{ formatRecordDuration(recordDuration) }}</text>
|
||||
<text class="recording-tip">{{ isCancelRecording ? '松开取消' : '上滑取消发送' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 输入框 -->
|
||||
<view class="input-wrap">
|
||||
<wd-input
|
||||
v-model="inputValue"
|
||||
type="textarea"
|
||||
:auto-height="true"
|
||||
:rows="1"
|
||||
placeholder="输入消息..."
|
||||
no-border
|
||||
@input="onInput"
|
||||
@confirm="onSend"
|
||||
/>
|
||||
</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>
|
||||
|
||||
<!-- 发送按钮 -->
|
||||
<wd-button
|
||||
v-if="inputValue.trim()"
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="onSend"
|
||||
>
|
||||
发送
|
||||
</wd-button>
|
||||
<wd-icon
|
||||
v-else
|
||||
name="add-circle"
|
||||
size="48rpx"
|
||||
class="tool-icon"
|
||||
@click="$emit('more')"
|
||||
/>
|
||||
<!-- 中间: 输入框 -->
|
||||
<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 { useTheme } from '@/composables/useTheme'
|
||||
import AppAvatar from '@/components/common/AppAvatar.vue'
|
||||
import type { GroupMember } from '@/types/api'
|
||||
|
||||
@@ -87,6 +139,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
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
|
||||
@@ -94,9 +147,21 @@ const emit = defineEmits<{
|
||||
(e: 'mention', member: GroupMember): void
|
||||
}>()
|
||||
|
||||
const { isDark } = useTheme()
|
||||
|
||||
// 文本输入相关
|
||||
const inputValue = ref(props.modelValue)
|
||||
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 = ''
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
inputValue.value = val
|
||||
})
|
||||
@@ -105,6 +170,7 @@ watch(inputValue, (val) => {
|
||||
emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
// ========== 文本输入处理 (保持原有逻辑) ==========
|
||||
function onInput(e: any) {
|
||||
const value = e.detail?.value || e.value || inputValue.value
|
||||
// 检测 @ 符号
|
||||
@@ -119,7 +185,6 @@ function openMentionPicker() {
|
||||
|
||||
function selectMention(member: GroupMember) {
|
||||
const name = member.nickname || member.user?.name || ''
|
||||
// 如果已经输入了 @,就替换;否则追加 @名字
|
||||
if (inputValue.value.endsWith('@')) {
|
||||
inputValue.value = inputValue.value + name + ' '
|
||||
} else {
|
||||
@@ -134,37 +199,367 @@ function onSend() {
|
||||
emit('send', inputValue.value.trim())
|
||||
inputValue.value = ''
|
||||
}
|
||||
|
||||
// ========== 语音录制处理 ==========
|
||||
function initRecorderManager() {
|
||||
if (recorderManager) return recorderManager
|
||||
|
||||
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) {
|
||||
clearInterval(recordTimer)
|
||||
recordTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
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'
|
||||
})
|
||||
}
|
||||
|
||||
function onRecordingMove(e: TouchEvent) {
|
||||
if (!isRecording.value) return
|
||||
|
||||
const currentY = e.touches[0].clientY
|
||||
const diff = startY - currentY
|
||||
|
||||
// 上滑超过80px触发取消
|
||||
isCancelRecording.value = diff > 80
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.message-input {
|
||||
--input-bg: #f1f1f1;
|
||||
--input-text: #1f2937;
|
||||
--placeholder-color: #9ca3af;
|
||||
--icon-color: #6b7280;
|
||||
--icon-hover: #4F46E5;
|
||||
--send-bg: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%);
|
||||
--border-color: rgba(0, 0, 0, 0.05);
|
||||
--bg-content: #ffffff;
|
||||
|
||||
position: relative;
|
||||
background: var(--bg-content);
|
||||
border-top: 1rpx solid var(--border-color);
|
||||
|
||||
&.theme-dark {
|
||||
--input-bg: #292524;
|
||||
--input-text: #f5f5f4;
|
||||
--placeholder-color: #78716c;
|
||||
--icon-color: #a8a29e;
|
||||
--icon-hover: #f97316;
|
||||
--send-bg: linear-gradient(135deg, #fb923c 0%, #f97316 100%);
|
||||
--border-color: #44403c;
|
||||
--bg-content: #1c1917;
|
||||
}
|
||||
}
|
||||
|
||||
// 底部输入栏
|
||||
.input-bar {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
padding: 16rpx 20rpx;
|
||||
padding-bottom: calc(16rpx + env(safe-area-inset-bottom));
|
||||
background: var(--bg-content);
|
||||
border-top: 1rpx solid var(--divider-color);
|
||||
gap: 16rpx;
|
||||
gap: 24rpx;
|
||||
padding: 24rpx 32rpx;
|
||||
padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.input-toolbar {
|
||||
// 语音按钮
|
||||
.voice-btn {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
padding-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.tool-icon {
|
||||
color: var(--text-secondary);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 16rpx;
|
||||
color: var(--icon-color);
|
||||
transition: color 0.15s;
|
||||
|
||||
svg {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: var(--icon-hover);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
// 输入框容器
|
||||
.input-wrap {
|
||||
flex: 1;
|
||||
background: var(--search-bg);
|
||||
border-radius: 16rpx;
|
||||
padding: 8rpx 16rpx;
|
||||
min-height: 88rpx;
|
||||
background: var(--input-bg);
|
||||
border-radius: 40rpx;
|
||||
padding: 20rpx 28rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: all 0.2s;
|
||||
|
||||
.theme-dark & {
|
||||
border: 1rpx solid var(--border-color);
|
||||
}
|
||||
}
|
||||
|
||||
// @ 提及选择器
|
||||
.text-input {
|
||||
width: 100%;
|
||||
min-height: 48rpx;
|
||||
max-height: 200rpx;
|
||||
font-size: 30rpx;
|
||||
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;
|
||||
}
|
||||
|
||||
.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);
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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);
|
||||
@@ -174,12 +569,26 @@ function onSend() {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx 30rpx;
|
||||
border-bottom: 1rpx solid var(--divider-color);
|
||||
border-bottom: 1rpx solid var(--border-color);
|
||||
|
||||
.title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,20 +603,13 @@ function onSend() {
|
||||
gap: 20rpx;
|
||||
|
||||
&:active {
|
||||
background: var(--bg-hover);
|
||||
background: var(--input-bg);
|
||||
}
|
||||
|
||||
.mention-name {
|
||||
font-size: 30rpx;
|
||||
color: var(--text-primary);
|
||||
color: var(--input-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -55,3 +55,9 @@ withDefaults(defineProps<Props>(), {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<view class="app-tabbar">
|
||||
<view class="app-tabbar" :class="{ dark: isDark }">
|
||||
<view class="tabbar-wrapper">
|
||||
<view
|
||||
v-for="tab in tabs"
|
||||
@@ -9,13 +9,13 @@
|
||||
@click="switchTab(tab.name)"
|
||||
>
|
||||
<view class="tabbar-icon">
|
||||
<!-- 消息图标 -->
|
||||
<!-- 消息图标 (message-circle) -->
|
||||
<template v-if="tab.name === 'messages'">
|
||||
<svg v-if="current === tab.name" class="icon" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z"/>
|
||||
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/>
|
||||
</svg>
|
||||
<svg v-else class="icon" 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 v-else class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/>
|
||||
</svg>
|
||||
<!-- 未读角标 -->
|
||||
<view v-if="unreadCount > 0" class="badge">
|
||||
@@ -23,27 +23,31 @@
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 联系人图标 -->
|
||||
<!-- 联系人图标 (users) -->
|
||||
<template v-else-if="tab.name === 'contacts'">
|
||||
<svg v-if="current === tab.name" class="icon" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="9" cy="7" r="4" fill="currentColor"/>
|
||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
|
||||
</svg>
|
||||
<svg v-else class="icon" 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 v-else class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="9" cy="7" r="4"/>
|
||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<!-- 朋友圈图标 -->
|
||||
<!-- 发现图标 (compass) -->
|
||||
<template v-else-if="tab.name === 'moments'">
|
||||
<svg v-if="current === tab.name" class="icon" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
|
||||
<circle cx="12" cy="12" r="10" fill="currentColor"/>
|
||||
<polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76" fill="white"/>
|
||||
</svg>
|
||||
<svg v-else class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<svg v-else class="icon" 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"/>
|
||||
<polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/>
|
||||
</svg>
|
||||
</template>
|
||||
</view>
|
||||
@@ -56,6 +60,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useConversationStore } from '@/stores/conversation'
|
||||
import { useTheme } from '@/composables/useTheme'
|
||||
|
||||
interface Props {
|
||||
current?: string
|
||||
@@ -65,19 +70,19 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
current: 'messages'
|
||||
})
|
||||
|
||||
const { isDark } = useTheme()
|
||||
const conversationStore = useConversationStore()
|
||||
const unreadCount = computed(() => conversationStore.totalUnread)
|
||||
|
||||
// 底部导航只有3个: 消息/联系人/发现 (无"我的")
|
||||
const tabs = [
|
||||
{ name: 'messages', label: '消息', path: '/pages/index/index' },
|
||||
{ name: 'contacts', label: '联系人', path: '/pages/contact/index' },
|
||||
{ name: 'moments', label: '朋友圈', path: '/pages/moment/index' }
|
||||
{ name: 'moments', label: '发现', path: '/pages/moment/index' }
|
||||
]
|
||||
|
||||
function switchTab(name: string) {
|
||||
// 点击当前 tab 不跳转
|
||||
if (props.current === name) return
|
||||
|
||||
const tab = tabs.find(t => t.name === name)
|
||||
if (tab) {
|
||||
uni.reLaunch({ url: tab.path })
|
||||
@@ -86,20 +91,25 @@ function switchTab(name: string) {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// Tab Bar 容器 (高度 84px = 168rpx)
|
||||
.app-tabbar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
background: var(--bg-content, #fff);
|
||||
border-top: 1rpx solid var(--divider-color, #eee);
|
||||
z-index: 100;
|
||||
height: 168rpx;
|
||||
background: #ffffff;
|
||||
border-top: 1rpx solid rgba(0, 0, 0, 0.05);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.tabbar-wrapper {
|
||||
display: flex;
|
||||
height: 100rpx;
|
||||
align-items: center;
|
||||
padding-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.tabbar-item {
|
||||
@@ -108,12 +118,13 @@ function switchTab(name: string) {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4rpx;
|
||||
color: #999;
|
||||
gap: 8rpx;
|
||||
color: #9ca3af;
|
||||
transition: color 0.2s;
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
color: #07c160;
|
||||
color: #4F46E5;
|
||||
}
|
||||
|
||||
&:active {
|
||||
@@ -137,126 +148,45 @@ function switchTab(name: string) {
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
top: -8rpx;
|
||||
right: -20rpx;
|
||||
top: -12rpx;
|
||||
right: -24rpx;
|
||||
min-width: 32rpx;
|
||||
height: 32rpx;
|
||||
padding: 0 8rpx;
|
||||
background: #fa5151;
|
||||
background: #ef4444;
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
line-height: 32rpx;
|
||||
text-align: center;
|
||||
border-radius: 32rpx;
|
||||
white-space: nowrap;
|
||||
font-weight: 700;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tabbar-text {
|
||||
font-size: 22rpx;
|
||||
font-size: 20rpx;
|
||||
line-height: 1;
|
||||
font-weight: 500;
|
||||
|
||||
.tabbar-item.active & {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 深色模式 (Warm Stone)
|
||||
// ==========================================
|
||||
.app-tabbar.dark {
|
||||
background: #1c1917;
|
||||
border-top-color: #44403c;
|
||||
|
||||
.tabbar-item {
|
||||
color: #78716c;
|
||||
|
||||
&.active {
|
||||
color: #f97316;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<!--/*-->
|
||||
<!--<template>-->
|
||||
<!-- <view :class="{ dark: isDark }">-->
|
||||
<!-- <wd-tabbar-->
|
||||
<!-- :modelValue="current"-->
|
||||
<!-- fixed-->
|
||||
<!-- placeholder-->
|
||||
<!-- safe-area-inset-bottom-->
|
||||
<!-- bordered-->
|
||||
<!-- custom-class="app-tab-bar"-->
|
||||
<!-- @change="handleChange"-->
|
||||
<!-- >-->
|
||||
<!-- <wd-tabbar-item-->
|
||||
<!-- v-for="(item, index) in list"-->
|
||||
<!-- :key="index"-->
|
||||
<!-- :name="item.pagePath"-->
|
||||
<!-- :title="item.text"-->
|
||||
<!-- :value="item.badge > 0 ? item.badge : null"-->
|
||||
<!-- :icon="item.icon"-->
|
||||
<!-- >-->
|
||||
<!-- </wd-tabbar-item>-->
|
||||
<!-- </wd-tabbar>-->
|
||||
<!-- </view>-->
|
||||
<!--</template>-->
|
||||
|
||||
<!--<script setup lang="ts">-->
|
||||
<!--import { computed } from 'vue'-->
|
||||
<!--import { useTheme } from '@/composables/useTheme'-->
|
||||
<!--// 保持原有的 store 引用-->
|
||||
<!--import { useAppStore } from '@/stores'-->
|
||||
|
||||
<!--const props = defineProps<{-->
|
||||
<!-- active?: string-->
|
||||
<!--}>()-->
|
||||
|
||||
<!--const { isDark } = useTheme()-->
|
||||
<!--const appStore = useAppStore()-->
|
||||
|
||||
<!--// 核心修改:移除 image 路径,改用 Wot-UI设计 内置 Icon name-->
|
||||
<!--// 如果需要自定义 SVG,可以使用 custom-icon 插槽-->
|
||||
<!--const list = computed(() => [-->
|
||||
<!-- {-->
|
||||
<!-- pagePath: '/pages/index/index',-->
|
||||
<!-- text: '消息',-->
|
||||
<!-- icon: 'chat', // 对应 wd-icon name="chat"-->
|
||||
<!-- badge: appStore.unreadCount || 0-->
|
||||
<!-- },-->
|
||||
<!-- {-->
|
||||
<!-- pagePath: '/pages/contact/index',-->
|
||||
<!-- text: '通讯录',-->
|
||||
<!-- icon: 'user-circle', // 对应 wd-icon name="user-circle"-->
|
||||
<!-- badge: appStore.contactUnread || 0-->
|
||||
<!-- },-->
|
||||
<!-- {-->
|
||||
<!-- pagePath: '/pages/moment/index',-->
|
||||
<!-- text: '发现',-->
|
||||
<!-- icon: 'camera', // 对应 wd-icon name="camera"-->
|
||||
<!-- badge: appStore.momentUnread ? 1 : 0-->
|
||||
<!-- },-->
|
||||
<!-- {-->
|
||||
<!-- pagePath: '/pages/profile/index',-->
|
||||
<!-- text: '我的',-->
|
||||
<!-- icon: 'user', // 对应 wd-icon name="user"-->
|
||||
<!-- badge: 0-->
|
||||
<!-- }-->
|
||||
<!--])-->
|
||||
|
||||
<!--const current = computed(() => {-->
|
||||
<!-- if (props.active) return props.active-->
|
||||
<!-- const pages = getCurrentPages()-->
|
||||
<!-- const page = pages[pages.length - 1]-->
|
||||
<!-- return '/' + page.route-->
|
||||
<!--})-->
|
||||
|
||||
<!--function handleChange({ value }: { value: string }) {-->
|
||||
<!-- uni.switchTab({ url: value })-->
|
||||
<!--}-->
|
||||
<!--</script>-->
|
||||
|
||||
<!--<style lang="scss" scoped>-->
|
||||
<!--:deep(.app-tab-bar) {-->
|
||||
<!-- background-color: var(--bg-content) !important;-->
|
||||
|
||||
<!-- // 覆盖选中态颜色,保持与原有设计一致-->
|
||||
<!-- .wd-tabbar-item.is-active {-->
|
||||
<!-- color: var(--color-primary) !important;-->
|
||||
<!-- }-->
|
||||
<!--}-->
|
||||
|
||||
<!--.dark {-->
|
||||
<!-- :deep(.app-tab-bar) {-->
|
||||
<!-- background-color: #1c1c1e !important;-->
|
||||
<!-- border-top-color: #333;-->
|
||||
|
||||
<!-- .wd-tabbar-item {-->
|
||||
<!-- color: var(--text-secondary);-->
|
||||
<!-- &.is-active {-->
|
||||
<!-- color: var(--color-primary);-->
|
||||
<!-- }-->
|
||||
<!-- }-->
|
||||
<!-- }-->
|
||||
<!--}-->
|
||||
<!--</style>-->
|
||||
<!--*/-->
|
||||
|
||||
@@ -6,6 +6,9 @@ export { default as AppLoading } from './common/AppLoading.vue'
|
||||
export { default as UserInfoCard } from './common/UserInfoCard.vue'
|
||||
export { default as VideoPlayer } from './common/VideoPlayer.vue'
|
||||
export { default as ImagePreview } from './common/ImagePreview.vue'
|
||||
export { default as AppDrawer } from './common/AppDrawer.vue'
|
||||
export { default as PlusMenu } from './common/PlusMenu.vue'
|
||||
export { default as AppTabBar } from './common/AppTabBar.vue'
|
||||
|
||||
// 聊天组件
|
||||
export { default as MessageBubble } from './chat/MessageBubble.vue'
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,106 +1,180 @@
|
||||
<template>
|
||||
<wd-config-provider :theme="isDark ? 'dark' : 'light'">
|
||||
<view class="page-container" :class="{ 'theme-dark': isDark }">
|
||||
<!-- 顶部背景装饰 -->
|
||||
<view class="page-bg-decoration" />
|
||||
<view class="page-container" :class="{ 'theme-dark': isDark }">
|
||||
<!-- 渐变弧形头部 -->
|
||||
<view class="curved-header">
|
||||
<view class="header-bg"></view>
|
||||
<view class="decoration-orb orb-1"></view>
|
||||
<view class="decoration-orb orb-2"></view>
|
||||
|
||||
<!-- 导航栏 -->
|
||||
<app-nav-bar title="添加好友" custom-style="background: transparent;" />
|
||||
<!-- 返回按钮 -->
|
||||
<view class="nav-back" @click="goBack">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M15 18l-6-6 6-6"/>
|
||||
</svg>
|
||||
</view>
|
||||
|
||||
<view class="content-body">
|
||||
<!-- 头部标题区 -->
|
||||
<view class="header-area">
|
||||
<text class="main-title">发现新朋友</text>
|
||||
<text class="sub-title">Expanding your circle</text>
|
||||
<!-- 大标题 -->
|
||||
<view class="header-content">
|
||||
<text class="main-title">发现新朋友</text>
|
||||
<text class="sub-title">EXPANDING YOUR CIRCLE</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 悬浮搜索框 -->
|
||||
<view class="search-floater">
|
||||
<view class="search-box">
|
||||
<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"/>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
||||
</svg>
|
||||
<input
|
||||
class="search-input"
|
||||
type="text"
|
||||
v-model="keyword"
|
||||
placeholder="搜索账号 / 手机号"
|
||||
@confirm="doSearch"
|
||||
/>
|
||||
<view v-if="keyword" class="clear-btn" @click="keyword = ''">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm5 13.59L15.59 17 12 13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z"/>
|
||||
</svg>
|
||||
</view>
|
||||
<view v-else class="search-btn" @click="doSearch">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="5" y1="12" x2="19" y2="12"/>
|
||||
<polyline points="12 5 19 12 12 19"/>
|
||||
</svg>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 主内容 -->
|
||||
<scroll-view scroll-y class="main-content custom-scrollbar">
|
||||
<!-- 搜索结果 -->
|
||||
<view v-if="searched" class="result-section animate-fade-in-up">
|
||||
<view class="section-header">
|
||||
<text class="section-title">搜索结果</text>
|
||||
</view>
|
||||
|
||||
<!-- 搜索区域 -->
|
||||
<view class="search-section">
|
||||
<wd-search
|
||||
v-model="keyword"
|
||||
placeholder="搜索账号 / 手机号"
|
||||
hide-cancel
|
||||
placeholder-left
|
||||
custom-class="glass-search"
|
||||
@search="doSearch"
|
||||
@clear="keyword = ''"
|
||||
<!-- Loading -->
|
||||
<view v-if="loading" class="loading-box">
|
||||
<wd-loading size="50rpx" :color="isDark ? '#f97316' : '#4F46E5'" />
|
||||
<text>正在寻找...</text>
|
||||
</view>
|
||||
|
||||
<!-- Empty -->
|
||||
<view v-else-if="results.length === 0" class="empty-box">
|
||||
<svg class="empty-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<circle cx="11" cy="11" r="8"/>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
||||
</svg>
|
||||
<text class="empty-text">未找到匹配的用户</text>
|
||||
</view>
|
||||
|
||||
<!-- List -->
|
||||
<view v-else class="user-list">
|
||||
<view
|
||||
v-for="(item, index) in results"
|
||||
:key="item.id"
|
||||
class="user-card animate-fade-in-up"
|
||||
:style="{ animationDelay: `${index * 50}ms` }"
|
||||
>
|
||||
<template #suffix>
|
||||
<view class="search-btn" @click="doSearch">
|
||||
<wd-icon name="search" size="36rpx" />
|
||||
</view>
|
||||
</template>
|
||||
</wd-search>
|
||||
<app-avatar
|
||||
:src="item.avatar"
|
||||
:name="item.name"
|
||||
:size="100"
|
||||
radius="28rpx"
|
||||
/>
|
||||
<view class="card-info">
|
||||
<text class="user-name">{{ item.name }}</text>
|
||||
<text class="user-id">ID: {{ item.id }}</text>
|
||||
</view>
|
||||
<view class="action-wrap" :class="{ added: addedIds.includes(item.id) }" @click.stop="addFriend(item)">
|
||||
<svg v-if="!addedIds.includes(item.id)" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.5">
|
||||
<line x1="12" y1="5" x2="12" y2="19"/>
|
||||
<line x1="5" y1="12" x2="19" y2="12"/>
|
||||
</svg>
|
||||
<svg v-else viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.5">
|
||||
<polyline points="20 6 9 17 4 12"/>
|
||||
</svg>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 初始态:扫一扫卡片 + 推荐用户 -->
|
||||
<view v-else class="initial-content">
|
||||
<view class="feature-card scan-card" @click="scanQrCode">
|
||||
<view class="card-content">
|
||||
<view class="icon-bubble">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2">
|
||||
<path d="M3 7V5a2 2 0 0 1 2-2h2"/>
|
||||
<path d="M17 3h2a2 2 0 0 1 2 2v2"/>
|
||||
<path d="M21 17v2a2 2 0 0 1-2 2h-2"/>
|
||||
<path d="M7 21H5a2 2 0 0 1-2-2v-2"/>
|
||||
<line x1="7" y1="12" x2="17" y2="12"/>
|
||||
</svg>
|
||||
</view>
|
||||
<view class="text-group">
|
||||
<text class="card-title">扫一扫</text>
|
||||
<text class="card-desc">扫描二维码名片</text>
|
||||
</view>
|
||||
</view>
|
||||
<svg class="arrow-icon" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.6)" stroke-width="2">
|
||||
<polyline points="9 18 15 12 9 6"/>
|
||||
</svg>
|
||||
</view>
|
||||
|
||||
<!-- 搜索结果 -->
|
||||
<view v-if="searched" class="result-section">
|
||||
<!-- 推荐用户列表 -->
|
||||
<view v-if="recommendUsers.length > 0" class="recommend-section animate-fade-in-up">
|
||||
<view class="section-header">
|
||||
<text class="section-title">搜索结果</text>
|
||||
<text class="section-title">为您推荐</text>
|
||||
</view>
|
||||
|
||||
<!-- Loading -->
|
||||
<view v-if="loading" class="loading-box">
|
||||
<wd-loading size="50rpx" color="#4f46e5" />
|
||||
<text>正在寻找...</text>
|
||||
</view>
|
||||
|
||||
<!-- Empty -->
|
||||
<view v-else-if="results.length === 0" class="empty-box">
|
||||
<text class="empty-icon">🔍</text>
|
||||
<text class="empty-text">未找到匹配的用户</text>
|
||||
</view>
|
||||
|
||||
<!-- List -->
|
||||
<view v-else class="user-list">
|
||||
<view class="user-list">
|
||||
<view
|
||||
v-for="item in results"
|
||||
v-for="(item, index) in recommendUsers"
|
||||
:key="item.id"
|
||||
class="user-card"
|
||||
class="user-card animate-fade-in-up"
|
||||
:style="{ animationDelay: `${index * 50}ms` }"
|
||||
>
|
||||
<app-avatar
|
||||
:src="item.avatar"
|
||||
:name="item.name"
|
||||
:size="110"
|
||||
radius="36rpx"
|
||||
:size="100"
|
||||
radius="28rpx"
|
||||
/>
|
||||
<view class="card-info">
|
||||
<text class="user-name">{{ item.name }}</text>
|
||||
<text class="user-id">ID: {{ item.id }}</text>
|
||||
</view>
|
||||
<view class="action-wrap" @click.stop="addFriend(item)">
|
||||
<wd-icon name="add" size="36rpx" color="#fff" />
|
||||
<view class="action-wrap" :class="{ added: addedIds.includes(item.id) }" @click.stop="addFriend(item)">
|
||||
<svg v-if="!addedIds.includes(item.id)" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.5">
|
||||
<line x1="12" y1="5" x2="12" y2="19"/>
|
||||
<line x1="5" y1="12" x2="19" y2="12"/>
|
||||
</svg>
|
||||
<svg v-else viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.5">
|
||||
<polyline points="20 6 9 17 4 12"/>
|
||||
</svg>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 扫码入口 (无搜索结果时显示) -->
|
||||
<view v-if="!searched" class="feature-cards">
|
||||
<view class="feature-card scan-card" @click="scanQrCode">
|
||||
<view class="card-content">
|
||||
<view class="icon-bubble">
|
||||
<wd-icon name="scan" size="48rpx" color="#fff" />
|
||||
</view>
|
||||
<view class="text-group">
|
||||
<text class="card-title">扫一扫</text>
|
||||
<text class="card-desc">扫描二维码名片</text>
|
||||
</view>
|
||||
</view>
|
||||
<wd-icon name="arrow-right" size="32rpx" color="rgba(255,255,255,0.6)" />
|
||||
</view>
|
||||
<!-- 加载推荐用户中 -->
|
||||
<view v-else-if="loadingRecommend" class="loading-box">
|
||||
<wd-loading size="50rpx" :color="isDark ? '#f97316' : '#4F46E5'" />
|
||||
<text>获取推荐...</text>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<wd-toast />
|
||||
<wd-message-box />
|
||||
</view>
|
||||
</wd-config-provider>
|
||||
<wd-toast />
|
||||
<wd-message-box />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import * as contactApi from '@/api/modules/contact'
|
||||
import { useToast, useMessage } from 'wot-design-uni'
|
||||
import { useTheme } from '@/composables/useTheme'
|
||||
@@ -109,117 +183,285 @@ import type { User } from '@/types/api'
|
||||
|
||||
// --- 保持逻辑完全不变 ---
|
||||
const toast = useToast(); const message = useMessage(); const { isDark } = useTheme();
|
||||
const keyword = ref(''); const loading = ref(false); const searched = ref(false); const results = ref<User[]>([]);
|
||||
const keyword = ref(''); const loading = ref(false); const searched = ref(false); const results = ref<User[]>([]); const addedIds = ref<string[]>([]);
|
||||
|
||||
// 推荐用户相关
|
||||
const recommendUsers = ref<User[]>([]); const loadingRecommend = ref(false);
|
||||
|
||||
function goBack() { uni.navigateBack() }
|
||||
async function doSearch() { if (!keyword.value.trim()) { toast.show('请输入搜索内容'); return } loading.value = true; searched.value = true; try { results.value = await contactApi.searchUsers(keyword.value.trim()) } catch (error) { console.error('搜索失败:', error); toast.error('搜索失败') } finally { loading.value = false } }
|
||||
async function addFriend(user: User) { try { const { value } = await message.prompt({ title: '添加好友.html', inputPlaceholder: '请输入验证消息', inputValue: '你好,我是...' }); await contactApi.addFriend({ to_user_id: user.id, message: value || '' }); toast.success('申请已发送') } catch (e: any) { if (e !== 'cancel') { toast.error('发送失败') } } }
|
||||
async function addFriend(user: User) {
|
||||
if (addedIds.value.includes(user.id)) { toast.show('已发送申请'); return }
|
||||
try { const { value } = await message.prompt({ title: '添加好友', inputPlaceholder: '请输入验证消息', inputValue: '你好,我是...' }); await contactApi.addFriend({ to_user_id: user.id, message: value || '' }); addedIds.value.push(user.id); toast.success('申请已发送') } catch (e: any) { if (e !== 'cancel') { toast.error('发送失败') } }
|
||||
}
|
||||
function scanQrCode() { uni.scanCode({ onlyFromCamera: false, success: (res) => { console.log('扫码结果:', res.result); toast.show('扫码功能开发中') }, fail: () => { toast.error('扫码失败') } }) }
|
||||
|
||||
// 加载推荐用户
|
||||
async function loadRecommendUsers() {
|
||||
loadingRecommend.value = true;
|
||||
try {
|
||||
// 尝试获取推荐用户,如果 API 不存在则使用空列表
|
||||
if (typeof contactApi.getRecommendUsers === 'function') {
|
||||
recommendUsers.value = await contactApi.getRecommendUsers();
|
||||
} else {
|
||||
// API 不存在时使用默认数据
|
||||
recommendUsers.value = [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取推荐用户失败:', error);
|
||||
recommendUsers.value = [];
|
||||
} finally {
|
||||
loadingRecommend.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadRecommendUsers();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// 页面容器 - 浅色模式
|
||||
.page-container {
|
||||
--bg-base: #F7F8FA;
|
||||
--bg-card: #FFFFFF;
|
||||
--text-main: #1D2129;
|
||||
--text-sub: #86909C;
|
||||
--primary: #4F46E5;
|
||||
--primary-gradient: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%);
|
||||
--shadow-sm: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
|
||||
--shadow-lg: 0 12rpx 32rpx rgba(79, 70, 229, 0.15);
|
||||
--bg-base: #f7f8fa;
|
||||
--bg-card: #ffffff;
|
||||
--text-main: #1d1d1f;
|
||||
--text-sub: #6b7280;
|
||||
--color-brand: #4F46E5;
|
||||
--gradient-start: #4b6cb7;
|
||||
--gradient-end: #8E9EFE;
|
||||
|
||||
min-height: 100vh;
|
||||
background: var(--bg-base);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
// 深色模式 - Warm Stone
|
||||
&.theme-dark {
|
||||
--bg-base: #121212; /* 高级灰 */
|
||||
--bg-card: #1C1C1E; /* 卡片灰 */
|
||||
--text-main: #F2F3F5;
|
||||
--text-sub: #86909C;
|
||||
--shadow-sm: 0 4rpx 16rpx rgba(0, 0, 0, 0.2);
|
||||
--bg-base: #1c1917;
|
||||
--bg-card: #292524;
|
||||
--text-main: #f5f5f4;
|
||||
--text-sub: #78716c;
|
||||
--color-brand: #f97316;
|
||||
--gradient-start: #7c2d12;
|
||||
--gradient-end: #b45309;
|
||||
}
|
||||
}
|
||||
|
||||
/* 背景装饰球 */
|
||||
.page-bg-decoration {
|
||||
position: absolute;
|
||||
top: -100rpx;
|
||||
right: -100rpx;
|
||||
width: 400rpx;
|
||||
height: 400rpx;
|
||||
background: radial-gradient(circle, rgba(79, 70, 229, 0.15) 0%, rgba(0,0,0,0) 70%);
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
// 动画
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20rpx);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.content-body {
|
||||
.animate-fade-in-up {
|
||||
animation: fadeInUp 0.4s ease-out forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.custom-scrollbar {
|
||||
&::-webkit-scrollbar { display: none; }
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
// 渐变弧形头部
|
||||
.curved-header {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 20rpx 32rpx;
|
||||
}
|
||||
height: 720rpx;
|
||||
overflow: hidden;
|
||||
|
||||
.header-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end));
|
||||
clip-path: ellipse(150% 100% at 50% 0%);
|
||||
}
|
||||
|
||||
.decoration-orb {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(70rpx);
|
||||
pointer-events: none;
|
||||
|
||||
&.orb-1 {
|
||||
width: 300rpx;
|
||||
height: 300rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
top: 100rpx;
|
||||
left: -50rpx;
|
||||
}
|
||||
|
||||
&.orb-2 {
|
||||
width: 250rpx;
|
||||
height: 250rpx;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
top: 200rpx;
|
||||
right: -80rpx;
|
||||
filter: blur(80rpx);
|
||||
}
|
||||
}
|
||||
|
||||
.nav-back {
|
||||
position: absolute;
|
||||
top: calc(var(--status-bar-height) + 16rpx);
|
||||
left: 24rpx;
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
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;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.header-content {
|
||||
position: absolute;
|
||||
bottom: 200rpx;
|
||||
left: 48rpx;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* 头部 */
|
||||
.header-area {
|
||||
margin: 20rpx 0 40rpx;
|
||||
.main-title {
|
||||
display: block;
|
||||
font-size: 48rpx;
|
||||
font-weight: 800;
|
||||
color: var(--text-main);
|
||||
letter-spacing: -1rpx;
|
||||
font-size: 72rpx;
|
||||
font-weight: 900;
|
||||
font-style: italic;
|
||||
color: #fff;
|
||||
letter-spacing: -2rpx;
|
||||
text-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: var(--text-sub);
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2rpx;
|
||||
margin-top: 8rpx;
|
||||
letter-spacing: 4rpx;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
}
|
||||
|
||||
/* 搜索栏定制 */
|
||||
.search-section {
|
||||
margin-bottom: 40rpx;
|
||||
// 悬浮搜索框
|
||||
.search-floater {
|
||||
position: relative;
|
||||
margin-top: -80rpx;
|
||||
padding: 0 32rpx;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
:deep(.glass-search) {
|
||||
background: var(--bg-card) !important;
|
||||
padding: 16rpx 24rpx !important;
|
||||
border-radius: 32rpx !important;
|
||||
box-shadow: var(--shadow-sm);
|
||||
|
||||
.wd-search__field {
|
||||
background: transparent !important;
|
||||
height: 60rpx;
|
||||
}
|
||||
|
||||
.wd-icon-search { color: var(--text-sub); }
|
||||
.uni-input-placeholder { color: var(--text-sub); opacity: 0.6; }
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
background: var(--primary-gradient);
|
||||
border-radius: 50%;
|
||||
.search-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
box-shadow: 0 4rpx 12rpx rgba(79, 70, 229, 0.3);
|
||||
&:active { transform: scale(0.95); }
|
||||
background: var(--bg-card);
|
||||
padding: 16rpx 24rpx;
|
||||
border-radius: 50rpx;
|
||||
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1);
|
||||
|
||||
.search-icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
color: var(--text-sub);
|
||||
margin-right: 16rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
height: 72rpx;
|
||||
font-size: 30rpx;
|
||||
color: var(--text-main);
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--text-sub);
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-sub);
|
||||
margin-right: 12rpx;
|
||||
|
||||
svg {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
}
|
||||
|
||||
&:active { opacity: 0.7; }
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4rpx 16rpx rgba(79, 70, 229, 0.3);
|
||||
flex-shrink: 0;
|
||||
transition: all 0.15s;
|
||||
|
||||
.theme-dark & {
|
||||
background: linear-gradient(135deg, #fb923c 0%, #f97316 100%);
|
||||
box-shadow: 0 4rpx 16rpx rgba(249, 115, 22, 0.3);
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 结果列表 */
|
||||
.section-header {
|
||||
margin-bottom: 24rpx;
|
||||
.section-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: var(--text-main);
|
||||
// 主内容
|
||||
.main-content {
|
||||
height: calc(100vh - 720rpx + 80rpx);
|
||||
padding: 40rpx 32rpx;
|
||||
}
|
||||
|
||||
// 结果区
|
||||
.result-section {
|
||||
.section-header {
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.section-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: var(--text-main);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,27 +474,34 @@ function scanQrCode() { uni.scanCode({ onlyFromCamera: false, success: (res) =>
|
||||
color: var(--text-sub);
|
||||
font-size: 26rpx;
|
||||
}
|
||||
.empty-icon { font-size: 60rpx; opacity: 0.5; }
|
||||
|
||||
.empty-icon {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
// 用户卡片
|
||||
.user-card {
|
||||
background: var(--bg-card);
|
||||
padding: 24rpx;
|
||||
border-radius: 32rpx;
|
||||
border-radius: 40rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: var(--shadow-sm);
|
||||
animation: slideUp 0.3s ease-out;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
|
||||
|
||||
.card-info {
|
||||
flex: 1;
|
||||
margin-left: 24rpx;
|
||||
|
||||
.user-name {
|
||||
display: block;
|
||||
font-size: 34rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.user-id {
|
||||
display: block;
|
||||
margin-top: 6rpx;
|
||||
@@ -266,23 +515,61 @@ function scanQrCode() { uni.scanCode({ onlyFromCamera: false, success: (res) =>
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 24rpx;
|
||||
background: var(--text-main); /* 黑色/白色按钮 */
|
||||
background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
box-shadow: 0 4rpx 12rpx rgba(79, 70, 229, 0.25);
|
||||
|
||||
.theme-dark & { background: #333; }
|
||||
.theme-dark & {
|
||||
background: linear-gradient(135deg, #fb923c 0%, #f97316 100%);
|
||||
box-shadow: 0 4rpx 12rpx rgba(249, 115, 22, 0.25);
|
||||
}
|
||||
|
||||
&:active { transform: scale(0.92); }
|
||||
svg {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
}
|
||||
|
||||
&.added {
|
||||
background: #10b981;
|
||||
box-shadow: 0 4rpx 12rpx rgba(16, 185, 129, 0.25);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.92);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 功能卡片 (扫一扫) */
|
||||
.feature-cards {
|
||||
margin-top: 20rpx;
|
||||
// 初始内容
|
||||
.initial-content {
|
||||
padding-top: 20rpx;
|
||||
}
|
||||
|
||||
// 推荐区
|
||||
.recommend-section {
|
||||
margin-top: 48rpx;
|
||||
|
||||
.section-header {
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.section-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: var(--text-main);
|
||||
}
|
||||
}
|
||||
|
||||
.user-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// 功能卡片
|
||||
.feature-card {
|
||||
position: relative;
|
||||
border-radius: 40rpx;
|
||||
@@ -291,14 +578,17 @@ function scanQrCode() { uni.scanCode({ onlyFromCamera: false, success: (res) =>
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-shadow: var(--shadow-lg);
|
||||
box-shadow: 0 12rpx 32rpx rgba(79, 70, 229, 0.2);
|
||||
|
||||
.theme-dark & {
|
||||
box-shadow: 0 12rpx 32rpx rgba(249, 115, 22, 0.2);
|
||||
}
|
||||
|
||||
&.scan-card {
|
||||
background: var(--primary-gradient);
|
||||
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end));
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 卡片内部纹理 */
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
@@ -306,7 +596,7 @@ function scanQrCode() { uni.scanCode({ onlyFromCamera: false, success: (res) =>
|
||||
left: -20%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 60%);
|
||||
background: radial-gradient(circle, rgba(255, 255, 255, 0.15) 0%, transparent 60%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -320,25 +610,44 @@ function scanQrCode() { uni.scanCode({ onlyFromCamera: false, success: (res) =>
|
||||
.icon-bubble {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
background: rgba(255,255,255,0.2);
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
border-radius: 30rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 28rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
backdrop-filter: blur(4px);
|
||||
|
||||
svg {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.text-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.card-title { font-size: 36rpx; font-weight: 700; margin-bottom: 6rpx; }
|
||||
.card-desc { font-size: 24rpx; opacity: 0.8; font-weight: 400; }
|
||||
|
||||
.card-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-size: 24rpx;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.arrow-icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from { opacity: 0; transform: translateY(20rpx); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,67 +4,116 @@
|
||||
<view class="profile-header">
|
||||
<view class="blur-bg" :style="headerBgStyle" />
|
||||
<view class="gradient-mask" />
|
||||
<app-nav-bar custom-style="background: transparent;" title-style="color: transparent;" />
|
||||
|
||||
<!-- 返回按钮 -->
|
||||
<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>
|
||||
|
||||
<!-- 主体内容 (浮动卡片) -->
|
||||
<scroll-view scroll-y class="main-content" v-if="!loading && contact">
|
||||
<scroll-view scroll-y class="main-content custom-scrollbar" v-if="!loading && contact">
|
||||
<view class="content-wrapper">
|
||||
|
||||
<!-- 用户身份卡片 -->
|
||||
<view class="identity-card">
|
||||
<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="180"
|
||||
radius="50%"
|
||||
custom-style="border: 6rpx solid var(--bg-card);"
|
||||
: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">
|
||||
<text class="meta-tag">ID: {{ contact.user?.id || '-' }}</text>
|
||||
<text class="meta-tag">{{ contact.user?.region || '未知地区' }}</text>
|
||||
<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 class="settings-group">
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="设置备注" is-link @click="editRemark">
|
||||
<template #icon><view class="icon-wrap color-blue"><wd-icon name="edit-1" size="32rpx" color="#fff"/></view></template>
|
||||
<template #value><text class="cell-text">{{ contact.remark_name || '未设置' }}</text></template>
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
<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>
|
||||
</view>
|
||||
|
||||
<!-- 设置组:交互 -->
|
||||
<view class="settings-group">
|
||||
<view class="settings-group animate-fade-in-up" style="animation-delay: 150ms;">
|
||||
<view class="group-label">隐私与通知</view>
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="置顶聊天" center>
|
||||
<template #icon><view class="icon-wrap color-orange"><wd-icon name="star" size="32rpx" color="#fff"/></view></template>
|
||||
<template #value>
|
||||
<wd-switch v-model="contact.is_top" size="24px" active-color="#4F46E5" @change="toggleTop" />
|
||||
</template>
|
||||
</wd-cell>
|
||||
<wd-cell title="消息免打扰" center>
|
||||
<template #icon><view class="icon-wrap color-purple"><wd-icon name="notification-off" size="32rpx" color="#fff"/></view></template>
|
||||
<template #value>
|
||||
<wd-switch v-model="contact.is_muted" size="24px" active-color="#4F46E5" @change="toggleMute" />
|
||||
</template>
|
||||
</wd-cell>
|
||||
<wd-cell title="加入黑名单" center>
|
||||
<template #icon><view class="icon-wrap color-red"><wd-icon name="forbid" size="32rpx" color="#fff"/></view></template>
|
||||
<template #value>
|
||||
<wd-switch v-model="contact.is_blocked" size="24px" active-color="#FA5151" @change="toggleBlock" />
|
||||
</template>
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
<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" />
|
||||
@@ -73,21 +122,28 @@
|
||||
|
||||
<!-- Loading -->
|
||||
<view v-else-if="loading" class="center-loading">
|
||||
<wd-loading size="60rpx" color="#4F46E5"/>
|
||||
<wd-loading size="60rpx" :color="isDark ? '#f97316' : '#4F46E5'"/>
|
||||
</view>
|
||||
|
||||
<!-- 底部悬浮操作栏 -->
|
||||
<view class="floating-dock" v-if="contact">
|
||||
<view class="dock-content">
|
||||
<view class="dock-btn secondary" @click="startAudioCall">
|
||||
<wd-icon name="phone-filled" size="44rpx" />
|
||||
<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"/>
|
||||
</svg>
|
||||
</view>
|
||||
<view class="dock-btn primary" @click="goChat">
|
||||
<wd-icon name="chat-filled" size="40rpx" />
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" 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>
|
||||
<view class="dock-btn secondary" @click="startVideoCall">
|
||||
<wd-icon name="video" size="44rpx" />
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polygon points="23 7 16 12 23 17 23 7"/>
|
||||
<rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>
|
||||
</svg>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -123,6 +179,7 @@ onLoad((options: any) => { contactId.value = options?.id || ''; userId.value = o
|
||||
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 loadContactByUserId() { loading.value = true; try { const contacts = await contactApi.getContacts(); const found = contacts.find((c: Contact) => c.contact_user_id === userId.value || c.user_id === userId.value); if (found) { contact.value = await contactApi.getContactDetail(found.id?.toString() || '') } else { toast.warning('未找到该联系人'); setTimeout(() => uni.navigateBack(), 1500) } } catch (error) { console.error('加载联系人失败:', error); toast.error('加载失败') } finally { loading.value = false } }
|
||||
function goBack() { uni.navigateBack() }
|
||||
function goChat() { if (!contact.value) return; uni.navigateTo({ url: `/pages/chat/index?targetId=${contact.value.contact_user_id}&roomId=${contact.value.room_id || ''}&name=${encodeURIComponent(contact.value.remark_name || contact.value.user?.name || '')}&avatar=${encodeURIComponent(contact.value.user?.avatar || '')}` }) }
|
||||
function startAudioCall() { if (!contact.value) return; uni.navigateTo({ url: `/pages/chat/index?targetId=${contact.value.contact_user_id}&roomId=${contact.value.room_id || ''}&name=${encodeURIComponent(contact.value.remark_name || contact.value.user?.name || '')}&avatar=${encodeURIComponent(contact.value.user?.avatar || '')}&callType=audio` }) }
|
||||
function startVideoCall() { if (!contact.value) return; uni.navigateTo({ url: `/pages/chat/index?targetId=${contact.value.contact_user_id}&roomId=${contact.value.room_id || ''}&name=${encodeURIComponent(contact.value.remark_name || contact.value.user?.name || '')}&avatar=${encodeURIComponent(contact.value.user?.avatar || '')}&callType=video` }) }
|
||||
@@ -134,26 +191,52 @@ async function deleteFriend() { try { await messageBox.confirm({ title: '提示'
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 高级灰配色 */
|
||||
// 页面容器 - 浅色模式
|
||||
.detail-container {
|
||||
--bg-base: #F5F7FA;
|
||||
--bg-card: #FFFFFF;
|
||||
--text-main: #1D1D1F;
|
||||
--text-sub: #86909C;
|
||||
--primary: #4F46E5;
|
||||
--bg-base: #f7f8fa;
|
||||
--bg-card: #ffffff;
|
||||
--text-main: #1d1d1f;
|
||||
--text-sub: #6b7280;
|
||||
--color-brand: #4F46E5;
|
||||
|
||||
min-height: 100vh;
|
||||
background: var(--bg-base);
|
||||
position: relative;
|
||||
|
||||
// 深色模式 - Warm Stone
|
||||
&.theme-dark {
|
||||
--bg-base: #121212; /* 深度灰 */
|
||||
--bg-card: #1C1C1E; /* 卡片灰 */
|
||||
--text-main: #F7F8FA;
|
||||
--text-sub: #888;
|
||||
--bg-base: #1c1917;
|
||||
--bg-card: #292524;
|
||||
--text-main: #f5f5f4;
|
||||
--text-sub: #78716c;
|
||||
--color-brand: #f97316;
|
||||
}
|
||||
}
|
||||
|
||||
// 动画
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20rpx);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in-up {
|
||||
animation: fadeInUp 0.4s ease-out forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.custom-scrollbar {
|
||||
&::-webkit-scrollbar { display: none; }
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
// 视差头部背景
|
||||
.profile-header {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -168,19 +251,50 @@ async function deleteFriend() { try { await messageBox.confirm({ title: '提示'
|
||||
height: 100%;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
filter: blur(50px);
|
||||
transform: scale(1.2);
|
||||
opacity: 0.8;
|
||||
.theme-dark & { opacity: 0.4; }
|
||||
filter: blur(40px);
|
||||
transform: scale(1.1);
|
||||
opacity: 0.7;
|
||||
|
||||
.theme-dark & {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
.gradient-mask {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(to bottom, transparent 0%, var(--bg-base) 90%);
|
||||
background: linear-gradient(to bottom, transparent 0%, var(--bg-base) 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 主内容
|
||||
.main-content {
|
||||
height: 100vh;
|
||||
position: relative;
|
||||
@@ -189,10 +303,10 @@ async function deleteFriend() { try { await messageBox.confirm({ title: '提示'
|
||||
|
||||
.content-wrapper {
|
||||
padding: 0 32rpx;
|
||||
padding-top: calc(180rpx + var(--status-bar-height));
|
||||
padding-top: calc(220rpx + var(--status-bar-height));
|
||||
}
|
||||
|
||||
/* 身份卡片 */
|
||||
// 身份卡片
|
||||
.identity-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -200,7 +314,7 @@ async function deleteFriend() { try { await messageBox.confirm({ title: '提示'
|
||||
margin-bottom: 60rpx;
|
||||
|
||||
.avatar-wrapper {
|
||||
box-shadow: 0 16rpx 40rpx rgba(0,0,0,0.1);
|
||||
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.15);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
@@ -210,10 +324,9 @@ async function deleteFriend() { try { await messageBox.confirm({ title: '提示'
|
||||
|
||||
.user-name {
|
||||
font-size: 48rpx;
|
||||
font-weight: 800;
|
||||
font-weight: 700;
|
||||
color: var(--text-main);
|
||||
margin-bottom: 16rpx;
|
||||
letter-spacing: -1rpx;
|
||||
}
|
||||
|
||||
.meta-row {
|
||||
@@ -223,49 +336,75 @@ async function deleteFriend() { try { await messageBox.confirm({ title: '提示'
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.meta-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: var(--text-sub);
|
||||
background: rgba(0,0,0,0.03);
|
||||
.theme-dark & { background: rgba(255,255,255,0.08); }
|
||||
padding: 6rpx 20rpx;
|
||||
border-radius: 12rpx;
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
padding: 10rpx 20rpx;
|
||||
border-radius: 20rpx;
|
||||
|
||||
.theme-dark & {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bio-text {
|
||||
font-size: 28rpx;
|
||||
color: var(--text-main);
|
||||
opacity: 0.6;
|
||||
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: 20rpx;
|
||||
margin-bottom: 16rpx;
|
||||
margin-left: 12rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.wd-cell-group) {
|
||||
border-radius: 32rpx;
|
||||
overflow: hidden;
|
||||
background: var(--bg-card);
|
||||
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.02);
|
||||
.settings-card {
|
||||
background: var(--bg-card);
|
||||
border-radius: 24rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 28rpx 28rpx;
|
||||
border-bottom: 1rpx solid rgba(0, 0, 0, 0.04);
|
||||
transition: background 0.15s;
|
||||
|
||||
.theme-dark & {
|
||||
border-bottom-color: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
:deep(.wd-cell) {
|
||||
background: transparent;
|
||||
padding: 36rpx 32rpx;
|
||||
&::after { left: 110rpx; background: rgba(0,0,0,0.05); }
|
||||
.theme-dark &::after { background: rgba(255,255,255,0.05); }
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
|
||||
.theme-dark & {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
}
|
||||
|
||||
.icon-wrap {
|
||||
@@ -277,78 +416,128 @@ async function deleteFriend() { try { await messageBox.confirm({ title: '提示'
|
||||
justify-content: center;
|
||||
margin-right: 24rpx;
|
||||
|
||||
&.color-blue { background: #007AFF; }
|
||||
&.color-orange { background: #FF9500; }
|
||||
&.color-purple { background: #5856D6; }
|
||||
&.color-red { background: #FF3B30; }
|
||||
svg {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
}
|
||||
|
||||
&.color-blue { background: #0ea5e9; }
|
||||
&.color-orange { background: #f97316; }
|
||||
&.color-purple { background: #8b5cf6; }
|
||||
&.color-red { background: #ef4444; }
|
||||
}
|
||||
|
||||
.cell-text {
|
||||
.item-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.item-title {
|
||||
font-size: 30rpx;
|
||||
color: var(--text-main);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.item-value {
|
||||
font-size: 26rpx;
|
||||
color: var(--text-sub);
|
||||
font-size: 28rpx;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.chevron {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
color: var(--text-sub);
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.spacer { height: 300rpx; }
|
||||
.spacer {
|
||||
height: 320rpx;
|
||||
}
|
||||
|
||||
/* 底部操作台 */
|
||||
// 底部操作台
|
||||
.floating-dock {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 30rpx 40rpx;
|
||||
padding-bottom: calc(30rpx + env(safe-area-inset-bottom));
|
||||
background: var(--bg-card);
|
||||
border-top: 1rpx solid rgba(0,0,0,0.05);
|
||||
border-radius: 48rpx 48rpx 0 0;
|
||||
box-shadow: 0 -10rpx 40rpx rgba(0,0,0,0.04);
|
||||
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);
|
||||
z-index: 100;
|
||||
|
||||
.theme-dark & { border-top-color: rgba(255,255,255,0.05); }
|
||||
.theme-dark & {
|
||||
border-top-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.dock-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 32rpx;
|
||||
margin-bottom: 30rpx;
|
||||
gap: 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.dock-btn {
|
||||
height: 110rpx;
|
||||
border-radius: 36rpx;
|
||||
height: 100rpx;
|
||||
border-radius: 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
transition: all 0.15s;
|
||||
|
||||
&:active { transform: scale(0.96); }
|
||||
svg {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
&.primary {
|
||||
flex: 2;
|
||||
background: var(--primary);
|
||||
background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%);
|
||||
color: #fff;
|
||||
gap: 16rpx;
|
||||
font-weight: 600;
|
||||
font-size: 34rpx;
|
||||
box-shadow: 0 10rpx 24rpx rgba(79, 70, 229, 0.2);
|
||||
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: rgba(0,0,0,0.04);
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
color: var(--text-main);
|
||||
.theme-dark & { background: rgba(255,255,255,0.08); }
|
||||
|
||||
.theme-dark & {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.danger-btn {
|
||||
text-align: center;
|
||||
|
||||
text {
|
||||
color: var(--text-sub);
|
||||
font-size: 26rpx;
|
||||
text-decoration: underline;
|
||||
padding: 10rpx 20rpx;
|
||||
opacity: 0.8;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
&:active text {
|
||||
opacity: 1;
|
||||
color: #ef4444;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,28 +5,36 @@
|
||||
<!-- 头部固定区域 -->
|
||||
<view class="fixed-header">
|
||||
<view class="nav-bar">
|
||||
<text class="nav-title">通讯录</text>
|
||||
<!-- 修复:添加好友按钮 -->
|
||||
<text class="nav-title">联系人</text>
|
||||
<view class="icon-btn" @click="goAddFriend">
|
||||
<wd-icon name="add-user" size="48rpx" color="var(--text-primary)" />
|
||||
<svg class="add-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="8.5" cy="7" r="4"/>
|
||||
<line x1="20" y1="8" x2="20" y2="14"/>
|
||||
<line x1="23" y1="11" x2="17" y2="11"/>
|
||||
</svg>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="search-box-wrap">
|
||||
<wd-search
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索联系人/群聊"
|
||||
disabled
|
||||
hide-cancel
|
||||
custom-style="background: var(--bg-surface);"
|
||||
@click="goSearch"
|
||||
/>
|
||||
<view class="search-inner" @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"/>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
||||
</svg>
|
||||
<text class="search-placeholder">搜索联系人/群聊</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分段控制器 -->
|
||||
<view class="tabs-wrapper">
|
||||
<view class="segmented-control">
|
||||
<view
|
||||
v-for="tab in tabs"
|
||||
class="segment-slider"
|
||||
:style="{ transform: `translateX(${activeTabIndex * 100}%)` }"
|
||||
/>
|
||||
<view
|
||||
v-for="(tab, index) in tabs"
|
||||
:key="tab.key"
|
||||
class="segment-item"
|
||||
:class="{ active: activeTab === tab.key }"
|
||||
@@ -40,53 +48,76 @@
|
||||
|
||||
<!-- 滚动内容 -->
|
||||
<scroll-view
|
||||
class="main-scroll"
|
||||
class="main-scroll custom-scrollbar"
|
||||
scroll-y
|
||||
:refresher-enabled="true"
|
||||
:refresher-triggered="refreshing"
|
||||
@refresherrefresh="onRefresh"
|
||||
>
|
||||
<!-- 修复:顶部间距,防止Grid被遮挡 -->
|
||||
<view class="scroll-spacer"></view>
|
||||
|
||||
<!-- 功能 Grid -->
|
||||
<view v-if="activeTab === 'groups'" class="feature-grid">
|
||||
<view class="feature-card" @click="goFriendRequests">
|
||||
<view class="icon-wrap is-orange"><wd-icon name="user-add" size="36rpx" color="#fff" /></view>
|
||||
<view class="icon-wrap is-orange">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2">
|
||||
<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="8.5" cy="7" r="4"/>
|
||||
<line x1="20" y1="8" x2="20" y2="14"/>
|
||||
<line x1="23" y1="11" x2="17" y2="11"/>
|
||||
</svg>
|
||||
</view>
|
||||
<view class="text-wrap">
|
||||
<text class="feature-title">新朋友</text>
|
||||
<wd-badge v-if="friendRequestCount > 0" :value="friendRequestCount" type="danger" />
|
||||
<text v-if="friendRequestCount > 0" class="badge">{{ friendRequestCount }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="feature-card" @click="goGroupNotify">
|
||||
<view class="icon-wrap is-blue"><wd-icon name="bell" size="36rpx" color="#fff" /></view>
|
||||
<view class="text-wrap"><text class="feature-title">群通知</text></view>
|
||||
<view class="icon-wrap is-blue">
|
||||
<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"/>
|
||||
</svg>
|
||||
</view>
|
||||
<view class="text-wrap">
|
||||
<text class="feature-title">群通知</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 列表内容 -->
|
||||
<view class="list-container">
|
||||
<view v-if="loading" class="loading-box"><wd-loading size="40rpx" /></view>
|
||||
<view v-if="loading" class="loading-box">
|
||||
<wd-loading size="40rpx" />
|
||||
</view>
|
||||
|
||||
<!-- 1. 分组 Tab -->
|
||||
<template v-else-if="activeTab === 'groups'">
|
||||
<view class="group-section-header">
|
||||
<text class="section-title">我的分组</text>
|
||||
<view class="add-group-btn" @click="showCreateGroupModal = true">
|
||||
<wd-icon name="add" size="24rpx" /> <text>新建</text>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="12" y1="5" x2="12" y2="19"/>
|
||||
<line x1="5" y1="12" x2="19" y2="12"/>
|
||||
</svg>
|
||||
<text>新建</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 未分组 -->
|
||||
<view v-if="ungroupedContacts.length" class="collapse-item">
|
||||
<view v-if="ungroupedContacts.length" class="collapse-item animate-fade-in-up">
|
||||
<view class="collapse-header" @click="toggleCollapse(0)">
|
||||
<wd-icon :name="collapsedIds.includes(0) ? 'arrow-right' : 'arrow-down'" size="24rpx" color="var(--text-tertiary)" />
|
||||
<view class="collapse-arrow" :class="{ collapsed: collapsedIds.includes(0) }">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="6 9 12 15 18 9"/>
|
||||
</svg>
|
||||
</view>
|
||||
<text class="collapse-title">未分组</text>
|
||||
<text class="collapse-count">{{ ungroupedContacts.length }}</text>
|
||||
</view>
|
||||
<view v-show="!collapsedIds.includes(0)" class="collapse-content">
|
||||
<view v-for="contact in ungroupedContacts" :key="contact.id" class="contact-cell" @click="goContactDetail(contact)" @longpress="handleContactLongPress(contact)">
|
||||
<app-avatar :src="contact.user?.avatar" :name="contact.remark_name || contact.user?.name" :size="80" radius="12rpx" />
|
||||
<app-avatar :src="contact.user?.avatar" :name="contact.remark_name || contact.user?.name" :size="80" radius="20rpx" />
|
||||
<view class="cell-body">
|
||||
<text class="cell-title">{{ contact.remark_name || contact.user?.name }}</text>
|
||||
<text class="cell-desc">{{ contact.user?.desc || '暂无签名' }}</text>
|
||||
@@ -96,15 +127,19 @@
|
||||
</view>
|
||||
|
||||
<!-- 自定义分组 -->
|
||||
<view v-for="group in contactGroups" :key="group.id" class="collapse-item">
|
||||
<view v-for="(group, index) in contactGroups" :key="group.id" class="collapse-item animate-fade-in-up" :style="{ animationDelay: `${(index + 1) * 50}ms` }">
|
||||
<view class="collapse-header" @click="toggleCollapse(group.id)" @longpress="handleGroupLongPress(group)">
|
||||
<wd-icon :name="collapsedIds.includes(group.id) ? 'arrow-right' : 'arrow-down'" size="24rpx" color="var(--text-tertiary)" />
|
||||
<view class="collapse-arrow" :class="{ collapsed: collapsedIds.includes(group.id) }">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="6 9 12 15 18 9"/>
|
||||
</svg>
|
||||
</view>
|
||||
<text class="collapse-title">{{ group.group_name }}</text>
|
||||
<text class="collapse-count">{{ getGroupContacts(group.id).length }}</text>
|
||||
</view>
|
||||
<view v-show="!collapsedIds.includes(group.id)" class="collapse-content">
|
||||
<view v-for="contact in getGroupContacts(group.id)" :key="contact.id" class="contact-cell" @click="goContactDetail(contact)" @longpress="handleContactLongPress(contact)">
|
||||
<app-avatar :src="contact.user?.avatar" :name="contact.remark_name || contact.user?.name" :size="80" radius="12rpx" />
|
||||
<app-avatar :src="contact.user?.avatar" :name="contact.remark_name || contact.user?.name" :size="80" radius="20rpx" />
|
||||
<view class="cell-body">
|
||||
<text class="cell-title">{{ contact.remark_name || contact.user?.name }}</text>
|
||||
<text class="cell-desc">{{ contact.user?.desc }}</text>
|
||||
@@ -116,11 +151,11 @@
|
||||
|
||||
<!-- 2. 好友 Tab (A-Z) -->
|
||||
<template v-else-if="activeTab === 'friends'">
|
||||
<view v-for="group in alphabetGroupedContacts" :key="group.letter" class="alpha-group">
|
||||
<view v-for="group in alphabetGroupedContacts" :key="group.letter" class="alpha-group" :id="`letter-${group.letter}`">
|
||||
<view class="alpha-header">{{ group.letter }}</view>
|
||||
<view class="alpha-list">
|
||||
<view v-for="contact in group.contacts" :key="contact.id" class="contact-cell" @click="goContactDetail(contact)">
|
||||
<app-avatar :src="contact.user?.avatar" :name="contact.remark_name || contact.user?.name" :size="80" radius="12rpx" />
|
||||
<app-avatar :src="contact.user?.avatar" :name="contact.remark_name || contact.user?.name" :size="80" radius="20rpx" />
|
||||
<view class="cell-body border-bottom">
|
||||
<text class="cell-title">{{ contact.remark_name || contact.user?.name }}</text>
|
||||
</view>
|
||||
@@ -129,48 +164,57 @@
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 3. 群聊 Tab (修复:显示所有群组) -->
|
||||
<!-- 3. 群聊 Tab -->
|
||||
<template v-else-if="activeTab === 'chats'">
|
||||
<!-- 我创建的 -->
|
||||
<view v-if="createdGroups.length" class="collapse-item">
|
||||
<view v-if="createdGroups.length" class="collapse-item animate-fade-in-up">
|
||||
<view class="collapse-header" @click="toggleCollapse('created')">
|
||||
<wd-icon :name="collapsedIds.includes('created') ? 'arrow-right' : 'arrow-down'" size="24rpx" color="var(--text-tertiary)" />
|
||||
<view class="collapse-arrow" :class="{ collapsed: collapsedIds.includes('created') }">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="6 9 12 15 18 9"/>
|
||||
</svg>
|
||||
</view>
|
||||
<text class="collapse-title">我创建的群</text>
|
||||
<text class="collapse-count">{{ createdGroups.length }}</text>
|
||||
</view>
|
||||
<view v-show="!collapsedIds.includes('created')" class="collapse-content">
|
||||
<view v-for="g in createdGroups" :key="g.room_id" class="contact-cell" @click="goGroupChat(g)">
|
||||
<app-avatar :src="g.room_avatar" :name="g.room_name" :size="80" radius="12rpx" />
|
||||
<app-avatar :src="g.room_avatar" :name="g.room_name" :size="80" radius="20rpx" />
|
||||
<view class="cell-body"><text class="cell-title">{{ g.room_name }}</text></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 我管理的 -->
|
||||
<view v-if="managedGroups.length" class="collapse-item">
|
||||
<view v-if="managedGroups.length" class="collapse-item animate-fade-in-up" style="animation-delay: 50ms;">
|
||||
<view class="collapse-header" @click="toggleCollapse('managed')">
|
||||
<wd-icon :name="collapsedIds.includes('managed') ? 'arrow-right' : 'arrow-down'" size="24rpx" color="var(--text-tertiary)" />
|
||||
<view class="collapse-arrow" :class="{ collapsed: collapsedIds.includes('managed') }">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="6 9 12 15 18 9"/>
|
||||
</svg>
|
||||
</view>
|
||||
<text class="collapse-title">我管理的群</text>
|
||||
<text class="collapse-count">{{ managedGroups.length }}</text>
|
||||
</view>
|
||||
<view v-show="!collapsedIds.includes('managed')" class="collapse-content">
|
||||
<view v-for="g in managedGroups" :key="g.room_id" class="contact-cell" @click="goGroupChat(g)">
|
||||
<app-avatar :src="g.room_avatar" :name="g.room_name" :size="80" radius="12rpx" />
|
||||
<app-avatar :src="g.room_avatar" :name="g.room_name" :size="80" radius="20rpx" />
|
||||
<view class="cell-body"><text class="cell-title">{{ g.room_name }}</text></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 我加入的 -->
|
||||
<view v-if="joinedGroups.length" class="collapse-item">
|
||||
<view v-if="joinedGroups.length" class="collapse-item animate-fade-in-up" style="animation-delay: 100ms;">
|
||||
<view class="collapse-header" @click="toggleCollapse('joined')">
|
||||
<wd-icon :name="collapsedIds.includes('joined') ? 'arrow-right' : 'arrow-down'" size="24rpx" color="var(--text-tertiary)" />
|
||||
<view class="collapse-arrow" :class="{ collapsed: collapsedIds.includes('joined') }">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="6 9 12 15 18 9"/>
|
||||
</svg>
|
||||
</view>
|
||||
<text class="collapse-title">我加入的群</text>
|
||||
<text class="collapse-count">{{ joinedGroups.length }}</text>
|
||||
</view>
|
||||
<view v-show="!collapsedIds.includes('joined')" class="collapse-content">
|
||||
<view v-for="g in joinedGroups" :key="g.room_id" class="contact-cell" @click="goGroupChat(g)">
|
||||
<app-avatar :src="g.room_avatar" :name="g.room_name" :size="80" radius="12rpx" />
|
||||
<app-avatar :src="g.room_avatar" :name="g.room_name" :size="80" radius="20rpx" />
|
||||
<view class="cell-body"><text class="cell-title">{{ g.room_name }}</text></view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -181,15 +225,18 @@
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 索引条 -->
|
||||
<!-- A-Z 索引条 -->
|
||||
<view v-if="activeTab === 'friends'" class="index-bar">
|
||||
<view v-for="l in letters" :key="l" class="index-item" @click.stop="scrollToLetter(l)">{{ l }}</view>
|
||||
<view v-for="l in letters" :key="l" class="index-item" @click.stop="scrollToLetter(l)">
|
||||
{{ l }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 弹窗组件 -->
|
||||
<wd-action-sheet v-model="showContactActions" :actions="contactActionItems" @select="onContactActionSelect" cancel-text="取消" />
|
||||
<wd-action-sheet v-model="showGroupActions" :actions="groupActionItems" @select="onGroupActionSelect" cancel-text="取消" />
|
||||
<wd-action-sheet v-model="showMoveGroupModal" :actions="moveGroupActions" @select="onMoveGroupSelect" cancel-text="取消" />
|
||||
|
||||
<wd-popup v-model="showCreateGroupModal" custom-style="border-radius: 24rpx; width: 80%; padding: 40rpx; background: var(--bg-surface);">
|
||||
<view class="modal-content">
|
||||
<view class="modal-title">新建分组</view>
|
||||
@@ -200,6 +247,7 @@
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
|
||||
<wd-popup v-model="showRenameGroupModal" custom-style="border-radius: 24rpx; width: 80%; padding: 40rpx; background: var(--bg-surface);">
|
||||
<view class="modal-content">
|
||||
<view class="modal-title">重命名分组</view>
|
||||
@@ -210,6 +258,7 @@
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
|
||||
<wd-popup v-model="showRemarkModal" custom-style="border-radius: 24rpx; width: 80%; padding: 40rpx; background: var(--bg-surface);">
|
||||
<view class="modal-content">
|
||||
<view class="modal-title">修改备注</view>
|
||||
@@ -221,6 +270,9 @@
|
||||
</view>
|
||||
</wd-popup>
|
||||
|
||||
<!-- 全局群通话组件 -->
|
||||
<global-call-provider />
|
||||
|
||||
<wd-toast />
|
||||
<wd-message-box />
|
||||
<app-tab-bar current="contacts" />
|
||||
@@ -238,6 +290,7 @@ import * as contactApi from '@/api/modules/contact'
|
||||
import * as roomApi from '@/api/modules/room'
|
||||
import AppAvatar from '@/components/common/AppAvatar.vue'
|
||||
import AppTabBar from '@/components/common/AppTabBar.vue'
|
||||
import GlobalCallProvider from '@/components/call/GlobalCallProvider.vue'
|
||||
import type { Contact, ContactGroup } from '@/types/api'
|
||||
|
||||
// --- 逻辑完全不变 ---
|
||||
@@ -247,7 +300,11 @@ const tabs = [{ key: 'groups', label: '分组' }, { key: 'friends', label: '好
|
||||
const contacts = computed(() => chatStore.contacts); const friendRequestCount = computed(() => contactStore.friendRequests.filter(r => r.status === 'pending').length); const ungroupedContacts = computed(() => contacts.value.filter(c => !c.group_id || c.group_id === 0)); function getGroupContacts(groupId: number): Contact[] { return contacts.value.filter(c => c.group_id === groupId) }
|
||||
const letters = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '#']; const alphabetGroupedContacts = computed(() => { const groups: Record<string, Contact[]> = {}; contacts.value.forEach(contact => { const name = contact.remark_name || contact.user?.name || ''; const firstChar = name.charAt(0).toUpperCase(); const letter = /[A-Z]/.test(firstChar) ? firstChar : '#'; if (!groups[letter]) groups[letter] = []; groups[letter].push(contact) }); return Object.keys(groups).sort((a, b) => (a === '#' ? 1 : b === '#' ? -1 : a.localeCompare(b))).map(letter => ({ letter, contacts: groups[letter] })) });
|
||||
const createdGroups = computed(() => groupChats.value.filter(g => g.category === 'created')); const managedGroups = computed(() => groupChats.value.filter(g => g.category === 'managed')); const joinedGroups = computed(() => groupChats.value.filter(g => g.category === 'joined'));
|
||||
const contactActionItems = [{ name: '修改备注', value: 'remark' }, { name: '移动到分组', value: 'move' }, { name: '删除好友', value: 'delete', color: '#fa5151' }]; const groupActionItems = [{ name: '重命名', value: 'rename' }, { name: '删除分组', value: 'delete', color: '#fa5151' }]; const moveGroupActions = computed(() => { const actions: any[] = [{ name: '未分组', value: 0 }]; contactGroups.value.forEach(g => { if (selectedContact.value?.group_id !== g.id) actions.push({ name: g.group_name, value: g.id }) }); return actions });
|
||||
const contactActionItems = [{ name: '修改备注', value: 'remark' }, { name: '移动到分组', value: 'move' }, { name: '删除好友', value: 'delete', color: '#ef4444' }]; const groupActionItems = [{ name: '重命名', value: 'rename' }, { name: '删除分组', value: 'delete', color: '#ef4444' }]; const moveGroupActions = computed(() => { const actions: any[] = [{ name: '未分组', value: 0 }]; contactGroups.value.forEach(g => { if (selectedContact.value?.group_id !== g.id) actions.push({ name: g.group_name, value: g.id }) }); return actions });
|
||||
|
||||
// 计算当前选中的标签索引
|
||||
const activeTabIndex = computed(() => tabs.findIndex(t => t.key === activeTab.value))
|
||||
|
||||
onMounted(() => loadData()); onShow(() => loadFriendRequests());
|
||||
async function loadData() { loading.value = true; try { const [contactList, groups, chats] = await Promise.all([contactApi.getContacts(), contactApi.getGroups(), roomApi.getUserGroups()]); chatStore.setContacts(contactList); contactGroups.value = groups; groupChats.value = chats || [] } catch (error) { console.error(error) } finally { loading.value = false } }
|
||||
async function loadFriendRequests() { try { const requests = await contactApi.getFriendRequests(); contactStore.setFriendRequests(requests) } catch {} }
|
||||
@@ -273,104 +330,495 @@ async function handleDeleteGroup() { try { await messageBox.confirm({title:'提
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// ==========================================
|
||||
// 页面容器 - 浅色模式 (与设计稿一致)
|
||||
// ==========================================
|
||||
.page-container {
|
||||
--bg-page: #f7f8fa;
|
||||
--bg-page: #ffffff;
|
||||
--bg-surface: #ffffff;
|
||||
--bg-highlight: #f2f3f5;
|
||||
--text-primary: #1f1f1f;
|
||||
--bg-highlight: #f5f5f5;
|
||||
--bg-group: rgba(0, 0, 0, 0.02);
|
||||
--text-primary: #1f2937;
|
||||
--text-secondary: #6b7280;
|
||||
--text-tertiary: #9ca3af;
|
||||
--border-color: rgba(0,0,0,0.05);
|
||||
--brand-color: #4f46e5;
|
||||
--tab-bg: #e5e7eb;
|
||||
--border-color: rgba(0, 0, 0, 0.05);
|
||||
--color-brand: #4F46E5;
|
||||
--tab-bg: #f1f1f1;
|
||||
--tab-active-bg: #ffffff;
|
||||
--search-bg: #f1f1f1;
|
||||
|
||||
min-height: 100vh;
|
||||
background: var(--bg-page);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 深色模式 - Warm Stone (与设计稿一致)
|
||||
// ==========================================
|
||||
.page-container.dark {
|
||||
--bg-page: #1c1c1e; /* Dark Grey */
|
||||
--bg-surface: #2c2c2e;
|
||||
--bg-highlight: #3a3a3c;
|
||||
--text-primary: #f2f2f7;
|
||||
--text-secondary: #aeaeb2;
|
||||
--text-tertiary: #636366;
|
||||
--border-color: rgba(255,255,255,0.1);
|
||||
--tab-bg: #2c2c2e;
|
||||
--tab-active-bg: #636366;
|
||||
--bg-page: #1c1917;
|
||||
--bg-surface: #1c1917;
|
||||
--bg-highlight: #44403c;
|
||||
--bg-group: rgba(255, 255, 255, 0.03);
|
||||
--text-primary: #f5f5f4;
|
||||
--text-secondary: #e7e5e4;
|
||||
--text-tertiary: #78716c;
|
||||
--border-color: #44403c;
|
||||
--color-brand: #f97316;
|
||||
--tab-bg: #292524;
|
||||
--tab-active-bg: #44403c;
|
||||
--search-bg: #292524;
|
||||
}
|
||||
|
||||
// 动画
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20rpx);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in-up {
|
||||
animation: fadeInUp 0.4s ease-out forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.custom-scrollbar {
|
||||
&::-webkit-scrollbar { display: none; }
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 固定头部 (与设计稿一致)
|
||||
// ==========================================
|
||||
.fixed-header {
|
||||
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
background: var(--bg-page);
|
||||
padding-top: var(--status-bar-height);
|
||||
border-bottom: 1rpx solid var(--border-color);
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
height: 88rpx; display: flex; align-items: center; justify-content: space-between; padding: 0 32rpx;
|
||||
.nav-title { font-size: 40rpx; font-weight: 700; }
|
||||
.icon-btn { &:active { opacity: 0.7; } }
|
||||
height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 40rpx 16rpx;
|
||||
|
||||
.nav-title {
|
||||
font-size: 42rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.5rpx;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: var(--search-bg);
|
||||
transition: all 0.15s;
|
||||
|
||||
&:active {
|
||||
background: var(--bg-highlight);
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.add-icon {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-box-wrap { padding: 10rpx 24rpx; }
|
||||
// 搜索栏
|
||||
.search-box-wrap {
|
||||
padding: 10rpx 32rpx;
|
||||
}
|
||||
|
||||
.search-inner {
|
||||
height: 80rpx;
|
||||
background: var(--search-bg);
|
||||
border-radius: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:active {
|
||||
background: var(--bg-highlight);
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.search-placeholder {
|
||||
font-size: 28rpx;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
// 分段控制器
|
||||
.tabs-wrapper {
|
||||
padding: 16rpx 32rpx 24rpx;
|
||||
}
|
||||
|
||||
.tabs-wrapper { padding: 16rpx 32rpx 24rpx; }
|
||||
.segmented-control {
|
||||
background: var(--tab-bg); border-radius: 16rpx; padding: 6rpx; display: flex; height: 72rpx;
|
||||
position: relative;
|
||||
background: var(--tab-bg);
|
||||
border-radius: 16rpx;
|
||||
padding: 6rpx;
|
||||
display: flex;
|
||||
height: 72rpx;
|
||||
|
||||
.segment-slider {
|
||||
position: absolute;
|
||||
top: 6rpx;
|
||||
left: 6rpx;
|
||||
width: calc((100% - 12rpx) / 3);
|
||||
height: calc(100% - 12rpx);
|
||||
background: var(--tab-active-bg);
|
||||
border-radius: 12rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.segment-item {
|
||||
flex: 1; display: flex; align-items: center; justify-content: center;
|
||||
font-size: 28rpx; font-weight: 500; color: var(--text-secondary); border-radius: 12rpx;
|
||||
transition: all 0.3s;
|
||||
&.active { background: var(--tab-active-bg); color: var(--text-primary); font-weight: 600; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.08); }
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
transition: color 0.3s;
|
||||
|
||||
&.active {
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main-scroll { margin-top: calc(var(--status-bar-height) + 290rpx); height: calc(100vh - var(--status-bar-height) - 290rpx); }
|
||||
// 滚动区
|
||||
.main-scroll {
|
||||
margin-top: calc(var(--status-bar-height) + 290rpx);
|
||||
height: calc(100vh - var(--status-bar-height) - 290rpx);
|
||||
}
|
||||
|
||||
.scroll-spacer {
|
||||
height: 20rpx;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 功能卡片 (与设计稿一致)
|
||||
// ==========================================
|
||||
.feature-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 24rpx;
|
||||
padding: 16rpx 32rpx 16rpx;
|
||||
}
|
||||
|
||||
/* 修复:Grid 顶部垫片 */
|
||||
.scroll-spacer { height: 20rpx; }
|
||||
.feature-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 24rpx; padding: 0 32rpx 32rpx; }
|
||||
.feature-card {
|
||||
background: var(--bg-surface); padding: 24rpx; border-radius: 20rpx; display: flex; align-items: center; gap: 20rpx;
|
||||
.icon-wrap { width: 80rpx; height: 80rpx; border-radius: 20rpx; display: flex; align-items: center; justify-content: center; &.is-orange { background: linear-gradient(135deg, #f97316, #fb923c); } &.is-blue { background: linear-gradient(135deg, #0ea5e9, #38bdf8); } }
|
||||
.text-wrap { flex: 1; display: flex; flex-direction: column; .feature-title { font-weight: 600; font-size: 28rpx; } }
|
||||
}
|
||||
background: var(--bg-surface);
|
||||
padding: 24rpx;
|
||||
border-radius: 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
|
||||
border: 1rpx solid var(--border-color);
|
||||
transition: transform 0.15s;
|
||||
|
||||
.list-container { padding: 0 32rpx; }
|
||||
.group-section-header {
|
||||
display: flex; align-items: center; justify-content: space-between; margin: 32rpx 0 16rpx;
|
||||
.section-title { font-size: 32rpx; font-weight: 700; color: var(--text-secondary); }
|
||||
.add-group-btn { display: flex; align-items: center; gap: 4rpx; font-size: 26rpx; color: var(--brand-color); background: rgba(79, 70, 229, 0.1); padding: 8rpx 20rpx; border-radius: 30rpx; }
|
||||
}
|
||||
|
||||
.collapse-item { margin-bottom: 24rpx; background: var(--bg-surface); border-radius: 20rpx; overflow: hidden; }
|
||||
.collapse-header {
|
||||
padding: 24rpx; display: flex; align-items: center; gap: 16rpx; &:active { background: var(--bg-highlight); }
|
||||
.collapse-title { flex: 1; font-weight: 600; font-size: 30rpx; }
|
||||
.collapse-count { color: var(--text-tertiary); font-size: 24rpx; }
|
||||
}
|
||||
|
||||
.contact-cell {
|
||||
padding: 20rpx 24rpx; display: flex; align-items: center; gap: 24rpx; &:active { background: var(--bg-highlight); }
|
||||
.cell-body {
|
||||
flex: 1; display: flex; flex-direction: column; padding-right: 20rpx;
|
||||
&.border-bottom { padding-bottom: 20rpx; border-bottom: 1rpx solid var(--border-color); }
|
||||
.dark & {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.icon-wrap {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
svg {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
}
|
||||
|
||||
&.is-orange {
|
||||
background: linear-gradient(135deg, #fb923c, #f97316);
|
||||
}
|
||||
|
||||
&.is-blue {
|
||||
background: linear-gradient(135deg, #38bdf8, #0ea5e9);
|
||||
}
|
||||
}
|
||||
|
||||
.text-wrap {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rpx;
|
||||
|
||||
.feature-title {
|
||||
font-weight: 700;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 20rpx;
|
||||
font-weight: 500;
|
||||
color: #ef4444;
|
||||
}
|
||||
}
|
||||
.cell-title { font-size: 30rpx; font-weight: 500; margin-bottom: 6rpx; }
|
||||
.cell-desc { font-size: 24rpx; color: var(--text-tertiary); }
|
||||
}
|
||||
|
||||
.alpha-group { margin-bottom: 30rpx; }
|
||||
.alpha-header { font-size: 24rpx; font-weight: 700; color: var(--brand-color); margin-bottom: 12rpx; }
|
||||
// 列表容器
|
||||
.list-container {
|
||||
padding: 0 32rpx;
|
||||
}
|
||||
|
||||
.loading-box {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 60rpx;
|
||||
}
|
||||
|
||||
// 分组头部
|
||||
.group-section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 32rpx 0 16rpx;
|
||||
|
||||
.section-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.add-group-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
font-size: 26rpx;
|
||||
color: var(--color-brand);
|
||||
background: rgba(79, 70, 229, 0.1);
|
||||
padding: 10rpx 20rpx;
|
||||
border-radius: 30rpx;
|
||||
transition: all 0.15s;
|
||||
|
||||
svg {
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
}
|
||||
|
||||
.dark & {
|
||||
background: rgba(249, 115, 22, 0.15);
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 折叠项 (与设计稿一致)
|
||||
// ==========================================
|
||||
.collapse-item {
|
||||
margin-bottom: 16rpx;
|
||||
background: var(--bg-group);
|
||||
border-radius: 24rpx;
|
||||
overflow: hidden;
|
||||
border: 1rpx solid var(--border-color);
|
||||
}
|
||||
|
||||
.collapse-header {
|
||||
padding: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
transition: background 0.15s;
|
||||
|
||||
&:active {
|
||||
background: var(--bg-highlight);
|
||||
}
|
||||
|
||||
.collapse-arrow {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
color: var(--text-tertiary);
|
||||
transition: transform 0.2s;
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&.collapsed {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.collapse-title {
|
||||
flex: 1;
|
||||
font-weight: 600;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
|
||||
.collapse-count {
|
||||
color: var(--text-tertiary);
|
||||
font-size: 24rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.collapse-content {
|
||||
border-top: 1rpx solid var(--border-color);
|
||||
}
|
||||
|
||||
// 联系人单元格
|
||||
.contact-cell {
|
||||
padding: 20rpx 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
transition: background 0.15s;
|
||||
|
||||
&:active {
|
||||
background: var(--bg-highlight);
|
||||
}
|
||||
|
||||
.cell-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-right: 20rpx;
|
||||
|
||||
&.border-bottom {
|
||||
padding-bottom: 20rpx;
|
||||
border-bottom: 1rpx solid var(--border-color);
|
||||
}
|
||||
}
|
||||
|
||||
.cell-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
|
||||
.cell-desc {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
// 字母分组
|
||||
.alpha-group {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.alpha-header {
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: var(--color-brand);
|
||||
margin-bottom: 12rpx;
|
||||
padding-left: 4rpx;
|
||||
}
|
||||
|
||||
.alpha-list {
|
||||
background: var(--bg-surface);
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// A-Z索引条
|
||||
.index-bar {
|
||||
position: fixed; right: 8rpx; top: 55%; transform: translateY(-50%);
|
||||
background: var(--bg-highlight); border-radius: 20rpx; padding: 16rpx 6rpx; z-index: 101;
|
||||
.index-item { font-size: 20rpx; font-weight: 600; padding: 2rpx 10rpx; text-align: center; color: var(--text-secondary); margin: 4rpx 0; &:active { color: var(--brand-color); } }
|
||||
position: fixed;
|
||||
right: 16rpx;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
border-radius: 20rpx;
|
||||
padding: 16rpx 8rpx;
|
||||
z-index: 101;
|
||||
|
||||
.dark & {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.index-item {
|
||||
font-size: 20rpx;
|
||||
font-weight: 600;
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
line-height: 32rpx;
|
||||
text-align: center;
|
||||
color: var(--text-secondary);
|
||||
margin: 2rpx 0;
|
||||
border-radius: 50%;
|
||||
transition: all 0.15s;
|
||||
|
||||
&:active {
|
||||
color: var(--color-brand);
|
||||
background: rgba(79, 70, 229, 0.1);
|
||||
|
||||
.dark & {
|
||||
background: rgba(249, 115, 22, 0.15);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content { display: flex; flex-direction: column; align-items: center; .modal-title { font-size: 34rpx; font-weight: 600; margin-bottom: 10rpx; color: var(--text-primary); } .modal-btns { display: flex; gap: 32rpx; width: 100%; justify-content: space-between; margin-top: 20rpx;} }
|
||||
.safe-area-spacer { height: 180rpx; }
|
||||
// 弹窗
|
||||
.modal-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.modal-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
margin-bottom: 10rpx;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.modal-btns {
|
||||
display: flex;
|
||||
gap: 32rpx;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.safe-area-spacer {
|
||||
height: 180rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,45 +2,58 @@
|
||||
<wd-config-provider :theme="isDark ? 'dark' : 'light'">
|
||||
<view class="page-container" :class="{ dark: isDark }">
|
||||
|
||||
<!-- 1. 沉浸式导航栏 -->
|
||||
<!-- 1. 顶部导航栏 (与设计稿完全一致) -->
|
||||
<view class="custom-navbar">
|
||||
<view class="navbar-bg"></view>
|
||||
<view class="navbar-content">
|
||||
<view class="nav-left-group" @click="openDrawer">
|
||||
<app-avatar
|
||||
:src="user?.avatar"
|
||||
:name="user?.name"
|
||||
:size="76"
|
||||
round
|
||||
custom-style="border: 2rpx solid var(--border-subtle);"
|
||||
/>
|
||||
<view class="nav-info">
|
||||
<text class="nav-title">{{ user?.name || '消息' }}</text>
|
||||
<view class="online-tag" v-if="!loading">
|
||||
<view class="dot"></view>在线
|
||||
<!-- 左侧: 头像 + 标题 -->
|
||||
<view class="nav-left" @click="openDrawer">
|
||||
<view class="avatar-container">
|
||||
<image
|
||||
v-if="user?.avatar"
|
||||
class="nav-avatar"
|
||||
:src="resolveImageUrl(user.avatar)"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view v-else class="nav-avatar avatar-placeholder">
|
||||
{{ (user?.name || '?').charAt(0) }}
|
||||
</view>
|
||||
<view class="online-dot" v-if="!loading"></view>
|
||||
</view>
|
||||
<text class="nav-title">消息</text>
|
||||
</view>
|
||||
|
||||
<!-- 右侧: 加号按钮 -->
|
||||
<view class="nav-right">
|
||||
<view class="icon-btn" @click="showPlusMenu = true">
|
||||
<wd-icon name="add" size="52rpx" color="var(--text-primary)" />
|
||||
<view class="icon-btn-circle" @click="showPlusMenu = true">
|
||||
<svg class="plus-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
</svg>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 2. 搜索栏 -->
|
||||
<!-- 2. 搜索栏 (扁平化设计) -->
|
||||
<view class="search-wrapper">
|
||||
<view class="search-inner" @click="goSearch">
|
||||
<wd-icon name="search" size="32rpx" color="var(--text-tertiary)" />
|
||||
<text class="search-placeholder">搜索</text>
|
||||
<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"
|
||||
class="conversation-list custom-scrollbar"
|
||||
scroll-y
|
||||
:refresher-enabled="true"
|
||||
:refresher-triggered="refreshing"
|
||||
@@ -51,52 +64,62 @@
|
||||
<text>连接中...</text>
|
||||
</view>
|
||||
|
||||
<wd-status-tip
|
||||
v-else-if="conversations.length === 0"
|
||||
image="message"
|
||||
tip="暂无消息,开始聊天吧"
|
||||
/>
|
||||
<view v-else-if="conversations.length === 0" class="empty-state">
|
||||
<svg class="empty-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
|
||||
<line x1="9" y1="10" x2="15" y2="10"/>
|
||||
</svg>
|
||||
<text class="empty-text">暂无消息</text>
|
||||
</view>
|
||||
|
||||
<template v-else>
|
||||
<wd-swipe-action
|
||||
v-for="item in conversations"
|
||||
v-for="(item, index) in conversations"
|
||||
:key="item.id"
|
||||
:disabled="false"
|
||||
>
|
||||
<view
|
||||
class="conversation-item"
|
||||
:class="{ 'is-top': item.is_top }"
|
||||
:style="{ animationDelay: `${index * 30}ms` }"
|
||||
@click="goChat(item)"
|
||||
@longpress="handleLongPress(item)"
|
||||
>
|
||||
<!-- 头像 -->
|
||||
<view class="avatar-wrap">
|
||||
<app-avatar
|
||||
:src="item.avatar"
|
||||
:name="item.name"
|
||||
:size="104"
|
||||
radius="20rpx"
|
||||
:badge="item.is_muted ? 0 : item.unread_count"
|
||||
:dot="item.is_muted && item.unread_count > 0"
|
||||
<image
|
||||
v-if="item.avatar"
|
||||
class="conv-avatar"
|
||||
:src="resolveImageUrl(item.avatar)"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view v-if="item.is_special_care" class="special-badge-wrap">
|
||||
<wd-icon name="heart-fill" size="20rpx" color="#fff" />
|
||||
<view v-else class="conv-avatar avatar-placeholder">
|
||||
{{ (item.name || '?').charAt(0) }}
|
||||
</view>
|
||||
<!-- 在线状态点 (单聊时显示) -->
|
||||
<view v-if="item.is_online && !item.is_group" class="online-indicator"></view>
|
||||
<!-- 未读红点/数字 -->
|
||||
<view v-if="!item.is_muted && item.unread_count > 0" class="unread-badge">
|
||||
{{ item.unread_count > 99 ? '99+' : item.unread_count }}
|
||||
</view>
|
||||
<view v-else-if="item.is_muted && item.unread_count > 0" class="unread-dot"></view>
|
||||
</view>
|
||||
|
||||
<!-- 内容区 -->
|
||||
<view class="content-wrap">
|
||||
<view class="row-top">
|
||||
<view class="title-box">
|
||||
<wd-icon v-if="item.is_top" name="pin" size="24rpx" class="pin-icon" />
|
||||
<text class="name text-ellipsis" :class="{ 'special-text': item.is_special_care }">
|
||||
{{ item.name || '未知' }}
|
||||
</text>
|
||||
</view>
|
||||
<text class="name text-ellipsis" :class="{ 'special-name': item.is_special_care }">
|
||||
{{ item.name || '未知' }}
|
||||
</text>
|
||||
<text class="time">{{ formatMessageTime(item.last_time) }}</text>
|
||||
</view>
|
||||
|
||||
<view class="row-bottom">
|
||||
<text class="msg-preview text-ellipsis">{{ item.last_message || '暂无消息' }}</text>
|
||||
<wd-icon v-if="item.is_muted" name="volume-mute" size="28rpx" class="mute-icon" />
|
||||
<svg v-if="item.is_muted" class="mute-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M11 5L6 9H2v6h4l5 4V5z"/>
|
||||
<line x1="23" y1="9" x2="17" y2="15"/>
|
||||
<line x1="17" y1="9" x2="23" y2="15"/>
|
||||
</svg>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -104,10 +127,15 @@
|
||||
<template #right>
|
||||
<view class="swipe-actions">
|
||||
<view class="action-btn top" @click.stop="toggleTop(item)">
|
||||
<wd-icon name="pin" size="40rpx" />
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M16 12V4h1V2H7v2h1v8l-2 2v2h5.2v6h1.6v-6H18v-2l-2-2z"/>
|
||||
</svg>
|
||||
</view>
|
||||
<view class="action-btn delete" @click.stop="deleteConversation(item)">
|
||||
<wd-icon name="delete" size="40rpx" />
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="3 6 5 6 21 6"></polyline>
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
|
||||
</svg>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -117,14 +145,13 @@
|
||||
<view class="safe-area-spacer"></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 弹窗组件 -->
|
||||
<wd-action-sheet
|
||||
<!-- 加号菜单 -->
|
||||
<plus-menu
|
||||
v-model="showPlusMenu"
|
||||
:actions="plusMenuActions"
|
||||
@select="onPlusMenuSelect"
|
||||
:z-index="9999"
|
||||
/>
|
||||
|
||||
<!-- 长按操作菜单 -->
|
||||
<wd-action-sheet
|
||||
v-model="showConvActions"
|
||||
:actions="convActionItems"
|
||||
@@ -134,33 +161,13 @@
|
||||
/>
|
||||
|
||||
<!-- 侧边抽屉 -->
|
||||
<wd-popup
|
||||
<app-drawer
|
||||
v-model="showDrawer"
|
||||
position="left"
|
||||
custom-style="width: 75%; height: 100%; background: var(--bg-surface);"
|
||||
:z-index="10000"
|
||||
>
|
||||
<view class="drawer-container">
|
||||
<view class="drawer-header-bg"></view>
|
||||
<view class="drawer-info">
|
||||
<app-avatar :src="user?.avatar" :name="user?.name" :size="140" round custom-style="border: 4rpx solid var(--bg-surface);" />
|
||||
<text class="drawer-username">{{ user?.name || '请登录' }}</text>
|
||||
<text class="drawer-bio">{{ user?.desc || '编辑个签,展示你的独特个性' }}</text>
|
||||
</view>
|
||||
@logout="logout"
|
||||
/>
|
||||
|
||||
<view class="drawer-menu-list">
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="个人资料" icon="user" is-link size="large" @click="goProfile" />
|
||||
<wd-cell title="设置" icon="setting" is-link size="large" @click="goSettings" />
|
||||
<wd-cell title="深色模式" icon="picture" :value="themeText" is-link size="large" @click="goTheme" />
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
|
||||
<view class="drawer-footer-btn">
|
||||
<wd-button type="info" plain block @click="logout">退出登录</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
<!-- 全局群通话组件 -->
|
||||
<global-call-provider />
|
||||
|
||||
<wd-toast />
|
||||
<wd-message-box :z-index="11000" />
|
||||
@@ -175,10 +182,13 @@ import { onShow } from '@dcloudio/uni-app'
|
||||
import { useAuthStore, useConversationStore } from '@/stores'
|
||||
import { useTheme } from '@/composables/useTheme'
|
||||
import { formatMessageTime } from '@/utils/format'
|
||||
import { resolveImageUrl } from '@/utils/image'
|
||||
import { useToast, useMessage } from 'wot-design-uni'
|
||||
import * as conversationApi from '@/api/modules/conversation'
|
||||
import AppAvatar from '@/components/common/AppAvatar.vue'
|
||||
import AppTabBar from '@/components/common/AppTabBar.vue'
|
||||
import AppDrawer from '@/components/common/AppDrawer.vue'
|
||||
import PlusMenu from '@/components/common/PlusMenu.vue'
|
||||
import GlobalCallProvider from '@/components/call/GlobalCallProvider.vue'
|
||||
import type { Conversation } from '@/types/conversation'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
@@ -187,7 +197,6 @@ const { isDark } = useTheme()
|
||||
|
||||
const toast = useToast()
|
||||
const messageBox = useMessage()
|
||||
const searchKeyword = ref('')
|
||||
const showPlusMenu = ref(false)
|
||||
const showDrawer = ref(false)
|
||||
const refreshing = ref(false)
|
||||
@@ -198,18 +207,6 @@ const user = computed(() => authStore.user)
|
||||
const conversations = computed(() => conversationStore.conversations)
|
||||
const loading = computed(() => conversationStore.loading)
|
||||
|
||||
const themeText = computed(() => {
|
||||
const mode = uni.getStorageSync('nl_im_theme_mode') || 'system'
|
||||
const texts: Record<string, string> = { system: '跟随系统', light: '浅色模式', dark: '深色模式' }
|
||||
return texts[mode] || '跟随系统'
|
||||
})
|
||||
|
||||
const plusMenuActions = [
|
||||
{ name: '创建群聊', value: 'createGroup' },
|
||||
{ name: '添加好友.html', value: 'addFriend' },
|
||||
{ name: '扫一扫', value: 'scan' }
|
||||
]
|
||||
|
||||
const convActionItems = computed(() => {
|
||||
if (!selectedConv.value) return []
|
||||
const conv = selectedConv.value
|
||||
@@ -220,7 +217,7 @@ const convActionItems = computed(() => {
|
||||
]
|
||||
if (!isGroup) items.push({ name: conv.is_special_care ? '取消特别关心' : '特别关心', value: 'toggleSpecial' })
|
||||
if (conv.unread_count && conv.unread_count > 0) items.push({ name: '标记已读', value: 'markRead' })
|
||||
items.push({ name: '删除会话', value: 'delete', color: '#fa5151' })
|
||||
items.push({ name: '删除会话', value: 'delete', color: '#ef4444' })
|
||||
return items
|
||||
})
|
||||
|
||||
@@ -247,11 +244,20 @@ function goChat(item: Conversation) {
|
||||
uni.navigateTo({ url: `/pages/chat/index?roomId=${item.room_id || ''}&targetId=${item.target_id}&name=${encodeURIComponent(item.name || '')}&avatar=${encodeURIComponent(item.avatar || '')}` })
|
||||
}
|
||||
function handleLongPress(item: Conversation) { selectedConv.value = item; showConvActions.value = true }
|
||||
async function onConvActionSelect(event: any) {
|
||||
// Wot-UI设计 select 事件通常返回 { item, index },或者直接是 detail 对象
|
||||
// 防御性获取 item
|
||||
const action = event.item || event
|
||||
|
||||
async function toggleTop(item: Conversation) {
|
||||
try {
|
||||
await conversationApi.updateConversation({ target_id: item.target_id, is_top: !item.is_top })
|
||||
item.is_top = !item.is_top
|
||||
conversationStore.sortConversations()
|
||||
toast.success(item.is_top ? '已置顶' : '已取消置顶')
|
||||
} catch (e) {
|
||||
toast.error('操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function onConvActionSelect(event: any) {
|
||||
const action = event.item || event
|
||||
if (!selectedConv.value) return
|
||||
const conv = selectedConv.value
|
||||
showConvActions.value = false
|
||||
@@ -284,156 +290,487 @@ async function deleteConversation(item: Conversation) {
|
||||
} catch (e: any) { if(e !== 'cancel') toast.error('删除失败') }
|
||||
}
|
||||
|
||||
// 修复:正确处理 select 事件参数
|
||||
function onPlusMenuSelect(event: any) {
|
||||
showPlusMenu.value = false
|
||||
// WotUI 的 ActionSheet select 事件参数结构可能是 { item: {...} }
|
||||
// 我们兼容直接传 item 和传 event.item 的情况
|
||||
const item = event.item || event
|
||||
|
||||
console.log('Selected menu item:', item) // 调试日志
|
||||
|
||||
if (item && item.value === 'createGroup') {
|
||||
function onPlusMenuSelect(key: string) {
|
||||
if (key === 'createGroup') {
|
||||
uni.navigateTo({ url: '/pages/group/create' })
|
||||
} else if (item && item.value === 'addFriend') {
|
||||
} else if (key === 'addFriend') {
|
||||
uni.navigateTo({ url: '/pages/contact/add' })
|
||||
} else if (item && item.value === 'scan') {
|
||||
} else if (key === 'scan') {
|
||||
uni.scanCode({ success: () => toast.show('扫码成功'), fail: () => toast.error('扫码失败') })
|
||||
}
|
||||
}
|
||||
|
||||
function openDrawer() { showDrawer.value = true }
|
||||
function goProfile() { showDrawer.value = false; uni.navigateTo({ url: '/pages/profile/index' }) }
|
||||
function goSettings() { showDrawer.value = false; uni.navigateTo({ url: '/pages/settings/index' }) }
|
||||
function goTheme() { showDrawer.value = false; uni.navigateTo({ url: '/pages/settings/theme' }) }
|
||||
|
||||
async function logout() {
|
||||
try { await messageBox.confirm({ title: '提示', msg: '确定退出?' }); showDrawer.value = false; authStore.logout() } catch {}
|
||||
try {
|
||||
await messageBox.confirm({ title: '提示', msg: '确定退出?' })
|
||||
showDrawer.value = false
|
||||
authStore.logout()
|
||||
} catch {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// ==========================================
|
||||
// 页面容器 - 浅色模式变量
|
||||
// ==========================================
|
||||
.page-container {
|
||||
--bg-page: #f7f8fa;
|
||||
--bg-page: #ffffff;
|
||||
--bg-surface: #ffffff;
|
||||
--bg-hover: #f5f5f5;
|
||||
--text-primary: #1d1d1f;
|
||||
--text-secondary: #86868b;
|
||||
--text-tertiary: #a1a1a6;
|
||||
--border-subtle: rgba(0,0,0,0.05);
|
||||
--nav-bg: rgba(255, 255, 255, 0.85);
|
||||
--color-brand: #4f46e5;
|
||||
--bg-top: rgba(0, 0, 0, 0.02);
|
||||
--text-primary: #1f2937;
|
||||
--text-secondary: #6b7280;
|
||||
--text-tertiary: #9ca3af;
|
||||
--border-color: rgba(0, 0, 0, 0.05);
|
||||
--nav-bg: rgba(255, 255, 255, 0.9);
|
||||
--color-brand: #4F46E5;
|
||||
--search-bg: #f1f1f1;
|
||||
--search-focus: rgba(79, 70, 229, 0.1);
|
||||
|
||||
min-height: 100vh;
|
||||
background: var(--bg-page);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 深色模式 - Warm Stone 暖色调
|
||||
// ==========================================
|
||||
.page-container.dark {
|
||||
--bg-page: #1c1c1e;
|
||||
--bg-surface: #2c2c2e;
|
||||
--bg-hover: #3a3a3c;
|
||||
--text-primary: #f2f2f7;
|
||||
--text-secondary: #aeaeb2;
|
||||
--text-tertiary: #636366;
|
||||
--border-subtle: rgba(255,255,255,0.1);
|
||||
--nav-bg: rgba(28, 28, 30, 0.85);
|
||||
--bg-page: #1c1917;
|
||||
--bg-surface: #1c1917;
|
||||
--bg-hover: #44403c;
|
||||
--bg-top: rgba(255, 255, 255, 0.03);
|
||||
--text-primary: #f5f5f4;
|
||||
--text-secondary: #e7e5e4;
|
||||
--text-tertiary: #78716c;
|
||||
--border-color: #44403c;
|
||||
--nav-bg: rgba(28, 25, 23, 0.9);
|
||||
--color-brand: #f97316;
|
||||
--search-bg: #292524;
|
||||
--search-focus: rgba(249, 115, 22, 0.1);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 动画
|
||||
// ==========================================
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20rpx);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.custom-scrollbar {
|
||||
&::-webkit-scrollbar { display: none; }
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 导航栏 (与设计稿一致)
|
||||
// ==========================================
|
||||
.custom-navbar {
|
||||
position: sticky; top: 0; z-index: 100;
|
||||
padding-top: var(--status-bar-height);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
padding-top: var(--status-bar-height, 0);
|
||||
|
||||
.navbar-bg {
|
||||
position: absolute; inset: 0;
|
||||
background: var(--nav-bg); backdrop-filter: blur(20px);
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
}
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--nav-bg);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border-bottom: 1rpx solid transparent;
|
||||
|
||||
.navbar-content {
|
||||
position: relative; height: 44px;
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 0 32rpx;
|
||||
}
|
||||
|
||||
.nav-left-group {
|
||||
display: flex; align-items: center;
|
||||
&:active { opacity: 0.7; }
|
||||
|
||||
.nav-info {
|
||||
display: flex; flex-direction: column; justify-content: center;
|
||||
margin-left: 16rpx;
|
||||
.nav-title { font-size: 34rpx; font-weight: 700; color: var(--text-primary); line-height: 1.2; }
|
||||
.online-tag {
|
||||
display: flex; align-items: center; margin-top: 4rpx; font-size: 22rpx; color: #34c759;
|
||||
.dot { width: 10rpx; height: 10rpx; background: #34c759; border-radius: 50%; margin-right: 6rpx; }
|
||||
}
|
||||
.dark & {
|
||||
border-bottom-color: rgba(68, 64, 60, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
width: 72rpx; height: 72rpx; display: flex; align-items: center; justify-content: center; border-radius: 50%;
|
||||
&:active { background: var(--bg-hover); }
|
||||
.navbar-content {
|
||||
position: relative;
|
||||
height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 40rpx 16rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// 左侧: 头像 + 标题
|
||||
.nav-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.15s;
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-container {
|
||||
position: relative;
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
}
|
||||
|
||||
.nav-avatar {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 16rpx;
|
||||
object-fit: cover;
|
||||
background: var(--search-bg);
|
||||
box-shadow: 0 0 0 2rpx rgba(0, 0, 0, 0.05);
|
||||
|
||||
.dark & {
|
||||
box-shadow: 0 0 0 2rpx rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--color-brand);
|
||||
color: #ffffff;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.online-dot {
|
||||
position: absolute;
|
||||
bottom: -4rpx;
|
||||
right: -4rpx;
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
background: #10b981;
|
||||
border: 4rpx solid var(--bg-surface);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 42rpx;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
letter-spacing: -0.5rpx;
|
||||
}
|
||||
|
||||
// 右侧: 加号按钮
|
||||
.nav-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.icon-btn-circle {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--search-bg);
|
||||
border-radius: 50%;
|
||||
transition: all 0.15s;
|
||||
|
||||
&:active {
|
||||
background: var(--bg-hover);
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.plus-icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 搜索栏 (扁平化设计)
|
||||
// ==========================================
|
||||
.search-wrapper {
|
||||
background: var(--bg-page); padding: 16rpx 32rpx;
|
||||
position: sticky; top: calc(44px + var(--status-bar-height)); z-index: 99;
|
||||
}
|
||||
.search-inner {
|
||||
height: 72rpx; background: var(--bg-surface); border-radius: 16rpx;
|
||||
display: flex; align-items: center; justify-content: center; gap: 12rpx;
|
||||
.search-placeholder { font-size: 28rpx; color: var(--text-tertiary); }
|
||||
padding: 0 40rpx 16rpx;
|
||||
background: var(--bg-page);
|
||||
}
|
||||
|
||||
.conversation-list { height: calc(100vh - 44px - var(--status-bar-height) - 104rpx); }
|
||||
.conversation-item {
|
||||
display: flex; padding: 24rpx 32rpx; background: var(--bg-surface);
|
||||
transition: background-color 0.2s;
|
||||
&:active { background: var(--bg-hover); }
|
||||
&.is-top { background: var(--bg-page); }
|
||||
.content-wrap { border-bottom: 1rpx solid var(--border-subtle); }
|
||||
&:last-child .content-wrap { border-bottom: none; }
|
||||
}
|
||||
.search-bar {
|
||||
position: relative;
|
||||
height: 80rpx;
|
||||
background: var(--search-bg);
|
||||
border-radius: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 24rpx;
|
||||
transition: all 0.2s;
|
||||
|
||||
.avatar-wrap {
|
||||
position: relative; margin-right: 24rpx; flex-shrink: 0;
|
||||
.special-badge-wrap {
|
||||
position: absolute; bottom: -6rpx; right: -6rpx;
|
||||
background: #ec4899; border: 4rpx solid var(--bg-surface); border-radius: 50%;
|
||||
width: 36rpx; height: 36rpx; display: flex; align-items: center; justify-content: center;
|
||||
&:active {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.content-wrap { flex: 1; min-width: 0; display: flex; flex-direction: column; justify-content: center; padding-right: 4rpx; }
|
||||
.row-top, .row-bottom { display: flex; align-items: center; justify-content: space-between; }
|
||||
.row-top { margin-bottom: 8rpx; }
|
||||
.title-box {
|
||||
display: flex; align-items: center; flex: 1;
|
||||
.pin-icon { color: var(--color-brand); margin-right: 8rpx; transform: rotate(45deg); }
|
||||
.name { font-size: 32rpx; font-weight: 600; color: var(--text-primary); &.special-text { color: #ec4899; } }
|
||||
.search-icon {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
color: var(--text-tertiary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 30rpx;
|
||||
color: var(--text-primary);
|
||||
margin-left: 16rpx;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 会话列表
|
||||
// ==========================================
|
||||
.conversation-list {
|
||||
height: calc(100vh - 88rpx - var(--status-bar-height, 0) - 96rpx - 168rpx);
|
||||
}
|
||||
|
||||
// 空状态
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 200rpx 0;
|
||||
color: var(--text-tertiary);
|
||||
opacity: 0.6;
|
||||
|
||||
.empty-icon {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载状态
|
||||
.loading-state {
|
||||
padding-top: 200rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
color: var(--text-tertiary);
|
||||
gap: 20rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 会话条目 (与设计稿完全一致)
|
||||
// ==========================================
|
||||
.conversation-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 28rpx 40rpx;
|
||||
gap: 24rpx;
|
||||
background: var(--bg-surface);
|
||||
transition: background-color 0.15s;
|
||||
animation: fadeInUp 0.4s ease-out forwards;
|
||||
opacity: 0;
|
||||
|
||||
&:active {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
&.is-top {
|
||||
background: var(--bg-top);
|
||||
}
|
||||
}
|
||||
|
||||
// 头像区域
|
||||
.avatar-wrap {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.conv-avatar {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
border-radius: 20rpx;
|
||||
object-fit: cover;
|
||||
background: var(--search-bg);
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
// 未读数字徽章
|
||||
.unread-badge {
|
||||
position: absolute;
|
||||
top: -12rpx;
|
||||
right: -12rpx;
|
||||
min-width: 32rpx;
|
||||
height: 32rpx;
|
||||
padding: 0 8rpx;
|
||||
background: #ef4444;
|
||||
color: #ffffff;
|
||||
font-size: 20rpx;
|
||||
font-weight: 700;
|
||||
line-height: 32rpx;
|
||||
text-align: center;
|
||||
border-radius: 32rpx;
|
||||
white-space: nowrap;
|
||||
box-shadow: 0 0 0 4rpx var(--bg-surface);
|
||||
}
|
||||
|
||||
// 未读红点 (免打扰)
|
||||
.unread-dot {
|
||||
position: absolute;
|
||||
top: -8rpx;
|
||||
right: -8rpx;
|
||||
width: 20rpx;
|
||||
height: 20rpx;
|
||||
background: #ef4444;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 0 4rpx var(--bg-surface);
|
||||
}
|
||||
|
||||
// 在线状态指示器
|
||||
.online-indicator {
|
||||
position: absolute;
|
||||
bottom: 4rpx;
|
||||
right: 4rpx;
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
background: #10b981;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 0 4rpx var(--bg-surface);
|
||||
}
|
||||
|
||||
// 内容区域
|
||||
.content-wrap {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
border-bottom: 1rpx solid var(--border-color);
|
||||
padding-bottom: 28rpx;
|
||||
margin-bottom: -28rpx;
|
||||
margin-left: 8rpx;
|
||||
|
||||
.conversation-item:last-child & {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.row-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.row-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
&.special-name {
|
||||
color: #ec4899;
|
||||
|
||||
.dark & {
|
||||
color: #f472b6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 22rpx;
|
||||
color: var(--text-tertiary);
|
||||
margin-left: 16rpx;
|
||||
flex-shrink: 0;
|
||||
font-feature-settings: "tnum";
|
||||
letter-spacing: -0.5rpx;
|
||||
}
|
||||
|
||||
.msg-preview {
|
||||
font-size: 26rpx;
|
||||
color: var(--text-secondary);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-right: 16rpx;
|
||||
}
|
||||
|
||||
.mute-icon {
|
||||
width: 28rpx;
|
||||
height: 28rpx;
|
||||
color: var(--text-tertiary);
|
||||
flex-shrink: 0;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 滑动操作
|
||||
// ==========================================
|
||||
.swipe-actions {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
.time { font-size: 24rpx; color: var(--text-tertiary); margin-left: 16rpx; font-feature-settings: "tnum"; }
|
||||
.msg-preview { font-size: 28rpx; color: var(--text-secondary); flex: 1; }
|
||||
|
||||
.swipe-actions { display: flex; height: 100%; }
|
||||
.action-btn {
|
||||
width: 140rpx; height: 100%; display: flex; align-items: center; justify-content: center; color: #fff;
|
||||
&.top { background: #4f46e5; } &.delete { background: #ef4444; }
|
||||
width: 140rpx;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
|
||||
svg {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
|
||||
&.top {
|
||||
background: #4F46E5;
|
||||
|
||||
.dark & {
|
||||
background: #f97316;
|
||||
}
|
||||
}
|
||||
|
||||
&.delete {
|
||||
background: #ef4444;
|
||||
}
|
||||
}
|
||||
|
||||
.drawer-container { height: 100%; display: flex; flex-direction: column; position: relative; overflow: hidden; background: var(--bg-surface); }
|
||||
.drawer-header-bg {
|
||||
position: absolute; top: 0; left: 0; width: 100%; height: 300rpx;
|
||||
background: linear-gradient(135deg, var(--color-brand), rgba(79, 70, 229, 0.2)); opacity: 0.1; z-index: 0;
|
||||
// 底部安全区
|
||||
.safe-area-spacer {
|
||||
height: 180rpx;
|
||||
}
|
||||
.drawer-info {
|
||||
position: relative; z-index: 1; padding: 100rpx 40rpx 60rpx; display: flex; flex-direction: column;
|
||||
.drawer-username { font-size: 40rpx; font-weight: 700; margin-top: 24rpx; color: var(--text-primary); }
|
||||
.drawer-bio { font-size: 26rpx; color: var(--text-secondary); margin-top: 12rpx; }
|
||||
|
||||
// 文本溢出省略
|
||||
.text-ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.drawer-menu-list { flex: 1; background: var(--bg-surface); }
|
||||
.drawer-footer-btn { padding: 40rpx; padding-bottom: calc(40rpx + env(safe-area-inset-bottom)); }
|
||||
.safe-area-spacer { height: 150rpx; }
|
||||
.loading-state { padding-top: 200rpx; display: flex; flex-direction: column; align-items: center; color: var(--text-tertiary); gap: 20rpx; font-size: 26rpx; }
|
||||
</style>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<image
|
||||
v-if="moment.user?.avatar"
|
||||
class="avatar"
|
||||
:src="moment.user.avatar"
|
||||
:src="resolveImageUrl(moment.user.avatar)"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view v-else class="avatar avatar-placeholder">
|
||||
@@ -82,7 +82,7 @@
|
||||
<image
|
||||
v-if="comment.user?.avatar"
|
||||
class="avatar"
|
||||
:src="comment.user.avatar"
|
||||
:src="resolveImageUrl(comment.user.avatar)"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view v-else class="avatar avatar-placeholder">
|
||||
@@ -124,6 +124,7 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useMomentStore } from '@/stores'
|
||||
import { formatTime } from '@/utils/format'
|
||||
import { resolveImageUrl } from '@/utils/image'
|
||||
import { parseMediaUrls } from '@/types/moment'
|
||||
import { useTheme } from '@/composables/useTheme'
|
||||
import AppAvatar from '@/components/common/AppAvatar.vue'
|
||||
|
||||
@@ -2,18 +2,18 @@
|
||||
<wd-config-provider :theme="isDark ? 'dark' : 'light'">
|
||||
<view class="moments-page" :class="{ dark: isDark }">
|
||||
<!-- 1. 沉浸式渐变导航栏 -->
|
||||
<!-- 使用动态背景色实现滚动渐变 -->
|
||||
<view class="nav-header" :style="{ background: `rgba(var(--bg-nav-rgb), ${navOpacity})`, borderBottom: navOpacity > 0.8 ? '1rpx solid var(--border-color)' : 'none' }">
|
||||
<view class="nav-content">
|
||||
<text class="nav-title" :style="{ opacity: navOpacity }">朋友圈</text>
|
||||
<text class="nav-title" :style="{ opacity: navOpacity }">发现</text>
|
||||
|
||||
<!-- 保留原有的消息通知逻辑:铃铛图标 + 红点 -->
|
||||
<view class="nav-right">
|
||||
<wd-badge :value="unreadCount" :max="99" v-if="unreadCount > 0">
|
||||
<!-- 根据背景透明度自动切换图标颜色 -->
|
||||
<wd-icon name="bell" size="44rpx" :color="iconColor" @click="goNotifications" />
|
||||
</wd-badge>
|
||||
<wd-icon v-else name="bell" size="44rpx" :color="iconColor" @click="goNotifications" />
|
||||
<view class="nav-icon-btn" @click="goNotifications">
|
||||
<svg class="icon" :style="{ color: iconColor }" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<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"/>
|
||||
</svg>
|
||||
<view v-if="unreadCount > 0" class="badge">{{ unreadCount > 99 ? '99+' : unreadCount }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -21,7 +21,7 @@
|
||||
<!-- 2. 内容滚动区 -->
|
||||
<scroll-view
|
||||
scroll-y
|
||||
class="scroll-container"
|
||||
class="scroll-container custom-scrollbar"
|
||||
:refresher-enabled="true"
|
||||
:refresher-triggered="refreshing"
|
||||
@scroll="onPageScroll"
|
||||
@@ -30,12 +30,12 @@
|
||||
>
|
||||
<!-- 封面图区域 -->
|
||||
<view class="cover-area">
|
||||
<image src="/static/images/moment-cover.jpg" mode="aspectFill" class="cover-img" />
|
||||
<image src="https://images.unsplash.com/photo-1470252649378-9c29740c9fa8?w=800&q=80" mode="aspectFill" class="cover-img" />
|
||||
<view class="cover-gradient"></view>
|
||||
<view class="user-entry">
|
||||
<text class="username">{{ user?.name || '我' }}</text>
|
||||
<view class="avatar-box">
|
||||
<app-avatar :src="user?.avatar" :name="user?.name" :size="140" radius="20rpx" />
|
||||
<app-avatar :src="user?.avatar" :name="user?.name" :size="160" radius="24rpx" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -43,18 +43,24 @@
|
||||
<!-- 列表内容 -->
|
||||
<view class="feed-list">
|
||||
<view v-if="loading && moments.length === 0" class="loading-state">
|
||||
<wd-loading />
|
||||
<wd-loading :color="isDark ? '#f97316' : '#4F46E5'" />
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
|
||||
<wd-status-tip v-else-if="moments.length === 0" tip="暂无动态" />
|
||||
<view v-else-if="moments.length === 0" class="empty-state">
|
||||
<svg class="empty-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<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>
|
||||
<text class="empty-text">暂无动态,快去发布吧</text>
|
||||
</view>
|
||||
|
||||
<template v-else>
|
||||
<!-- 循环变量名恢复为 moment,匹配原有逻辑 -->
|
||||
<view v-for="moment in moments" :key="moment.id" class="feed-item">
|
||||
<!-- 头像 (移除原代码中不存在的点击事件) -->
|
||||
<view v-for="(moment, index) in moments" :key="moment.id" class="feed-item animate-fade-in-up" :style="{ animationDelay: `${index * 50}ms` }">
|
||||
<view class="feed-avatar">
|
||||
<app-avatar :src="moment.user?.avatar" :name="moment.user?.name" :size="88" radius="12rpx" />
|
||||
<app-avatar :src="moment.user?.avatar" :name="moment.user?.name" :size="84" radius="24rpx" />
|
||||
</view>
|
||||
|
||||
<view class="feed-content">
|
||||
@@ -65,8 +71,8 @@
|
||||
<!-- 图片九宫格 -->
|
||||
<view v-if="moment.media_type === 1" class="media-grid" :class="`grid-${getImageCount(moment)}`">
|
||||
<view
|
||||
v-for="(url, index) in parseMediaUrls(moment.media_urls)"
|
||||
:key="index"
|
||||
v-for="(url, imgIndex) in parseMediaUrls(moment.media_urls)"
|
||||
:key="imgIndex"
|
||||
class="grid-img-wrap"
|
||||
:style="{ width: getImageSize(moment), height: getImageSize(moment) }"
|
||||
>
|
||||
@@ -75,7 +81,7 @@
|
||||
width="100%"
|
||||
height="100%"
|
||||
mode="aspectFill"
|
||||
radius="8rpx"
|
||||
radius="12rpx"
|
||||
enable-preview
|
||||
:preview-src-list="parseMediaUrls(moment.media_urls).map(resolveImageUrl)"
|
||||
/>
|
||||
@@ -97,25 +103,27 @@
|
||||
<view class="feed-footer">
|
||||
<text class="time">{{ formatTime(moment.created_at) }}</text>
|
||||
<view class="action-btn" @click="showActionMenu(moment)">
|
||||
<wd-icon name="more" size="32rpx" color="var(--color-primary)" />
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<circle cx="5" cy="12" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="19" cy="12" r="2"/>
|
||||
</svg>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 点赞评论区 -->
|
||||
<!-- 使用原有的判断逻辑 -->
|
||||
<view v-if="(moment.likes && moment.likes.length > 0) || (moment.comments && moment.comments.length > 0)" class="interaction-area">
|
||||
<view class="triangle"></view>
|
||||
|
||||
<!-- 点赞列表 -->
|
||||
<view v-if="moment.likes && moment.likes.length > 0" class="likes-list">
|
||||
<wd-icon name="heart-fill" size="26rpx" color="var(--color-primary)" />
|
||||
<svg class="heart-icon" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>
|
||||
</svg>
|
||||
<text class="names">{{ moment.likes.map(l => l.user?.name || '').join('、') }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 分割线 -->
|
||||
<view v-if="moment.likes?.length && moment.comments?.length" class="divider"></view>
|
||||
|
||||
<!-- 评论列表 (保留原有点击跳转逻辑,不新增回复方法) -->
|
||||
<!-- 评论列表 -->
|
||||
<view v-if="moment.comments?.length" class="comments-list">
|
||||
<view
|
||||
v-for="comment in moment.comments"
|
||||
@@ -132,7 +140,6 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<wd-loadmore :state="loadState" @loadmore="loadMore" />
|
||||
</template>
|
||||
</view>
|
||||
@@ -140,7 +147,10 @@
|
||||
|
||||
<!-- 悬浮发布按钮 -->
|
||||
<view class="fab-btn" @click="goPublish">
|
||||
<wd-icon name="camera" size="52rpx" color="#fff" />
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.5" 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>
|
||||
|
||||
<!-- 操作菜单 -->
|
||||
@@ -148,12 +158,15 @@
|
||||
v-model="showActions"
|
||||
:actions="actionItems"
|
||||
@select="onActionSelect"
|
||||
cancel-text="取消"
|
||||
/>
|
||||
|
||||
<!-- 全局群通话组件 -->
|
||||
<global-call-provider />
|
||||
|
||||
<wd-toast />
|
||||
<wd-message-box />
|
||||
|
||||
<!-- TabBar (保留) -->
|
||||
<app-tab-bar current="moments" />
|
||||
</view>
|
||||
</wd-config-provider>
|
||||
@@ -169,6 +182,8 @@ import { resolveImageUrl } from '@/utils/image'
|
||||
import { parseMediaUrls } from '@/types/moment'
|
||||
import { useToast, useMessage } from 'wot-design-uni'
|
||||
import AppTabBar from '@/components/common/AppTabBar.vue'
|
||||
import AppAvatar from '@/components/common/AppAvatar.vue'
|
||||
import GlobalCallProvider from '@/components/call/GlobalCallProvider.vue'
|
||||
import type { Moment } from '@/types/moment'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
@@ -177,27 +192,26 @@ const { isDark } = useTheme()
|
||||
const toast = useToast()
|
||||
const messageBox = useMessage()
|
||||
|
||||
// --- UI设计 状态 (新增) ---
|
||||
// UI 状态
|
||||
const navOpacity = ref(0)
|
||||
const iconColor = computed(() => {
|
||||
// 当导航栏变白时,图标变黑;否则为白色
|
||||
if (navOpacity.value > 0.6) return 'var(--text-main)'
|
||||
return '#ffffff'
|
||||
})
|
||||
|
||||
// --- 原有状态保留 ---
|
||||
// 原有状态保留
|
||||
const refreshing = ref(false)
|
||||
const showActions = ref(false)
|
||||
const currentMoment = ref<Moment | null>(null)
|
||||
const loadState = ref<'loading' | 'finished' | 'error'>('loading')
|
||||
|
||||
// --- 原有计算属性保留 ---
|
||||
// 原有计算属性保留
|
||||
const user = computed(() => authStore.user)
|
||||
const moments = computed(() => momentStore.moments)
|
||||
const loading = computed(() => momentStore.loading)
|
||||
const unreadCount = computed(() => momentStore.unreadCount)
|
||||
|
||||
// --- 原有菜单逻辑保留 ---
|
||||
// 原有菜单逻辑保留
|
||||
const actionItems = computed(() => {
|
||||
const items = []
|
||||
if (currentMoment.value) {
|
||||
@@ -207,29 +221,23 @@ const actionItems = computed(() => {
|
||||
})
|
||||
items.push({ name: '评论', value: 'comment' })
|
||||
if (currentMoment.value.user_id === user.value?.id) {
|
||||
items.push({ name: '删除', value: 'delete', color: '#fa5151' })
|
||||
items.push({ name: '删除', value: 'delete', color: '#ef4444' })
|
||||
}
|
||||
}
|
||||
return items
|
||||
})
|
||||
|
||||
// --- 生命周期 ---
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
// 生命周期
|
||||
onMounted(() => { loadData() })
|
||||
onShow(() => { loadUnreadCount() })
|
||||
|
||||
onShow(() => {
|
||||
loadUnreadCount()
|
||||
})
|
||||
|
||||
// --- 新增滚动监听 (UI逻辑) ---
|
||||
// 滚动监听
|
||||
function onPageScroll(e: any) {
|
||||
const scrollTop = e.detail.scrollTop
|
||||
// 滚动 200px 后导航栏完全不透明
|
||||
navOpacity.value = Math.min(scrollTop / 200, 1)
|
||||
}
|
||||
|
||||
// --- 原有方法保留 ---
|
||||
// 原有方法保留
|
||||
async function loadData() {
|
||||
try {
|
||||
await momentStore.fetchMoments()
|
||||
@@ -240,11 +248,7 @@ async function loadData() {
|
||||
}
|
||||
|
||||
async function loadUnreadCount() {
|
||||
try {
|
||||
await momentStore.fetchUnreadCount()
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
try { await momentStore.fetchUnreadCount() } catch {}
|
||||
}
|
||||
|
||||
async function onRefresh() {
|
||||
@@ -261,43 +265,23 @@ async function loadMore() {
|
||||
loadState.value = momentStore.pagination.hasMore ? 'loading' : 'finished'
|
||||
}
|
||||
|
||||
function goNotifications() {
|
||||
uni.navigateTo({ url: '/pages/moment/notify' })
|
||||
}
|
||||
|
||||
function goPublish() {
|
||||
uni.navigateTo({ url: '/pages/moment/publish' })
|
||||
}
|
||||
|
||||
function showActionMenu(moment: Moment) {
|
||||
currentMoment.value = moment
|
||||
showActions.value = true
|
||||
}
|
||||
function goNotifications() { uni.navigateTo({ url: '/pages/moment/notify' }) }
|
||||
function goPublish() { uni.navigateTo({ url: '/pages/moment/publish' }) }
|
||||
function showActionMenu(moment: Moment) { currentMoment.value = moment; showActions.value = true }
|
||||
|
||||
async function onActionSelect(item: { value: string }) {
|
||||
if (!currentMoment.value) return
|
||||
|
||||
switch (item.value) {
|
||||
case 'like':
|
||||
await toggleLike()
|
||||
break
|
||||
case 'comment':
|
||||
goComment()
|
||||
break
|
||||
case 'delete':
|
||||
await deleteMoment()
|
||||
break
|
||||
case 'like': await toggleLike(); break
|
||||
case 'comment': goComment(); break
|
||||
case 'delete': await deleteMoment(); break
|
||||
}
|
||||
showActions.value = false
|
||||
}
|
||||
|
||||
async function toggleLike() {
|
||||
if (!currentMoment.value) return
|
||||
try {
|
||||
await momentStore.toggleLike(currentMoment.value.id)
|
||||
} catch {
|
||||
toast.error('操作失败')
|
||||
}
|
||||
try { await momentStore.toggleLike(currentMoment.value.id) } catch { toast.error('操作失败') }
|
||||
}
|
||||
|
||||
function goComment() {
|
||||
@@ -308,18 +292,12 @@ function goComment() {
|
||||
async function deleteMoment() {
|
||||
if (!currentMoment.value) return
|
||||
try {
|
||||
await messageBox.confirm({
|
||||
title: '提示',
|
||||
msg: '确定要删除这条动态吗?'
|
||||
})
|
||||
await messageBox.confirm({ title: '提示', msg: '确定要删除这条动态吗?' })
|
||||
await momentStore.deleteMoment(currentMoment.value.id)
|
||||
toast.success('已删除')
|
||||
} catch {
|
||||
// 取消或失败
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// --- 原有辅助函数保留 ---
|
||||
function getImageCount(moment: Moment): number {
|
||||
const urls = parseMediaUrls(moment.media_urls)
|
||||
return Math.min(urls.length, 9)
|
||||
@@ -334,47 +312,70 @@ function getImageSize(moment: Moment): string {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// 页面容器 - 浅色模式
|
||||
.moments-page {
|
||||
/* 定义 CSS 变量以便暗黑模式切换 */
|
||||
--bg-page: #ffffff;
|
||||
--bg-nav-rgb: 255, 255, 255;
|
||||
--bg-detail: #f7f7f7;
|
||||
--text-main: #1d1d1f;
|
||||
--text-blue: #576b95;
|
||||
--color-primary: #576b95;
|
||||
--border-color: rgba(0,0,0,0.05);
|
||||
--color-brand: #4F46E5;
|
||||
--border-color: rgba(0, 0, 0, 0.05);
|
||||
|
||||
height: 100vh;
|
||||
background: var(--bg-page);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
// 深色模式 - Warm Stone
|
||||
.moments-page.dark {
|
||||
/* 调整深色模式颜色:
|
||||
使用 #1c1c1e (iOS 深色背景) 替代纯黑 #000000
|
||||
提升质感,减少 OLED 拖影
|
||||
*/
|
||||
--bg-page: #1c1c1e;
|
||||
--bg-nav-rgb: 28, 28, 30; /* 对应 #1c1c1e 的 RGB 值 */
|
||||
--bg-detail: #2c2c2e; /* 稍微亮一点的二级背景,用于评论区 */
|
||||
--text-main: #f2f2f7; /* 柔和的白色文字 */
|
||||
--bg-page: #1c1917;
|
||||
--bg-nav-rgb: 28, 25, 23;
|
||||
--bg-detail: #292524;
|
||||
--text-main: #f5f5f4;
|
||||
--text-blue: #7e95c5;
|
||||
--border-color: rgba(255,255,255,0.1);
|
||||
--color-brand: #f97316;
|
||||
--border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* 1. 沉浸式 Header */
|
||||
// 动画
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20rpx);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in-up {
|
||||
animation: fadeInUp 0.4s ease-out forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.custom-scrollbar {
|
||||
&::-webkit-scrollbar { display: none; }
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
// 沉浸式 Header
|
||||
.nav-header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: calc(44px + var(--status-bar-height));
|
||||
height: calc(88rpx + var(--status-bar-height));
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
align-items: flex-end; /* 内容底部对齐 */
|
||||
padding-bottom: 20rpx;
|
||||
align-items: flex-end;
|
||||
padding-bottom: 16rpx;
|
||||
justify-content: center;
|
||||
transition: background 0.3s;
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
.nav-content {
|
||||
@@ -383,82 +384,143 @@ function getImageSize(moment: Moment): string {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0 30rpx;
|
||||
padding: 0 32rpx;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-weight: 600;
|
||||
font-weight: 700;
|
||||
font-size: 34rpx;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.nav-right {
|
||||
position: absolute;
|
||||
right: 30rpx;
|
||||
right: 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav-icon-btn {
|
||||
position: relative;
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: all 0.15s;
|
||||
|
||||
&:active {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
top: 4rpx;
|
||||
right: 4rpx;
|
||||
min-width: 32rpx;
|
||||
height: 32rpx;
|
||||
padding: 0 8rpx;
|
||||
background: #ef4444;
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
font-weight: 600;
|
||||
line-height: 32rpx;
|
||||
text-align: center;
|
||||
border-radius: 32rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.scroll-container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 2. 封面区域 */
|
||||
// 封面区域
|
||||
.cover-area {
|
||||
position: relative;
|
||||
height: 600rpx;
|
||||
margin-bottom: 80rpx; /* 留出空间给头像 */
|
||||
height: 680rpx;
|
||||
margin-bottom: 100rpx;
|
||||
|
||||
.cover-img { width: 100%; height: 100%; }
|
||||
.cover-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 底部渐变遮罩,保证文字清晰 */
|
||||
.cover-gradient {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 160rpx;
|
||||
height: 200rpx;
|
||||
background: linear-gradient(to top, var(--bg-page), transparent);
|
||||
}
|
||||
|
||||
.user-entry {
|
||||
position: absolute;
|
||||
bottom: -40rpx;
|
||||
right: 30rpx;
|
||||
bottom: -60rpx;
|
||||
right: 32rpx;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 24rpx;
|
||||
align-items: flex-end;
|
||||
gap: 20rpx;
|
||||
|
||||
.username {
|
||||
font-weight: 600;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
text-shadow: 0 2rpx 4rpx rgba(0,0,0,0.5);
|
||||
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.5);
|
||||
font-size: 36rpx;
|
||||
margin-top: 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.avatar-box {
|
||||
border: 6rpx solid var(--bg-page);
|
||||
border-radius: 24rpx;
|
||||
border: 8rpx solid var(--bg-page);
|
||||
border-radius: 28rpx;
|
||||
background: var(--bg-page);
|
||||
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.1);
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 3. 动态列表 */
|
||||
// 动态列表
|
||||
.feed-list {
|
||||
padding: 0 30rpx;
|
||||
padding: 0 32rpx;
|
||||
}
|
||||
|
||||
.loading-state, .empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 100rpx;
|
||||
color: var(--text-blue);
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.feed-item {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
margin-bottom: 60rpx;
|
||||
margin-bottom: 48rpx;
|
||||
padding-bottom: 32rpx;
|
||||
border-bottom: 1rpx solid var(--border-color);
|
||||
padding-bottom: 30rpx;
|
||||
|
||||
&:last-child { border-bottom: none; }
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.feed-avatar {
|
||||
@@ -467,12 +529,12 @@ function getImageSize(moment: Moment): string {
|
||||
|
||||
.feed-content {
|
||||
flex: 1;
|
||||
min-width: 0; /* 防止文本溢出 */
|
||||
min-width: 0;
|
||||
|
||||
.nickname {
|
||||
color: var(--text-blue);
|
||||
font-weight: 600;
|
||||
font-size: 32rpx;
|
||||
font-size: 30rpx;
|
||||
margin-bottom: 12rpx;
|
||||
display: block;
|
||||
}
|
||||
@@ -487,59 +549,78 @@ function getImageSize(moment: Moment): string {
|
||||
}
|
||||
}
|
||||
|
||||
// 图片九宫格
|
||||
.media-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10rpx;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
.grid-img-wrap {
|
||||
overflow: hidden;
|
||||
border-radius: 8rpx;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.video-wrap {
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
.video-player {
|
||||
width: 400rpx;
|
||||
height: 300rpx;
|
||||
border-radius: 8rpx;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// 底部操作栏
|
||||
.feed-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 10rpx;
|
||||
margin-top: 12rpx;
|
||||
|
||||
.time { font-size: 24rpx; color: #999; }
|
||||
.time {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-blue);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 60rpx;
|
||||
height: 44rpx;
|
||||
background: var(--bg-detail);
|
||||
padding: 6rpx 16rpx;
|
||||
border-radius: 8rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
&:active { opacity: 0.6; }
|
||||
color: var(--text-blue);
|
||||
transition: all 0.15s;
|
||||
|
||||
svg {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 4. 交互区 (点赞评论) */
|
||||
// 交互区 (点赞评论)
|
||||
.interaction-area {
|
||||
margin-top: 20rpx;
|
||||
margin-top: 16rpx;
|
||||
background: var(--bg-detail);
|
||||
border-radius: 8rpx;
|
||||
border-radius: 12rpx;
|
||||
padding: 16rpx 20rpx;
|
||||
position: relative;
|
||||
|
||||
.triangle {
|
||||
position: absolute;
|
||||
top: -12rpx;
|
||||
left: 20rpx;
|
||||
width: 0; height: 0;
|
||||
left: 24rpx;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 12rpx solid transparent;
|
||||
border-right: 12rpx solid transparent;
|
||||
border-bottom: 12rpx solid var(--bg-detail);
|
||||
@@ -553,9 +634,24 @@ function getImageSize(moment: Moment): string {
|
||||
color: var(--text-blue);
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
|
||||
.heart-icon {
|
||||
width: 28rpx;
|
||||
height: 28rpx;
|
||||
flex-shrink: 0;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.divider { height: 1rpx; background: rgba(0,0,0,0.05); margin: 12rpx 0; }
|
||||
.divider {
|
||||
height: 1rpx;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
margin: 12rpx 0;
|
||||
|
||||
.dark & {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.comment-row {
|
||||
font-size: 28rpx;
|
||||
@@ -563,37 +659,40 @@ function getImageSize(moment: Moment): string {
|
||||
margin-bottom: 8rpx;
|
||||
line-height: 1.5;
|
||||
|
||||
&:last-child { margin-bottom: 0; }
|
||||
.user-link { color: var(--text-blue); font-weight: 500; }
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.user-link {
|
||||
color: var(--text-blue);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 5. 悬浮按钮 */
|
||||
// 悬浮发布按钮
|
||||
.fab-btn {
|
||||
position: fixed;
|
||||
bottom: calc(150rpx + env(safe-area-inset-bottom));
|
||||
bottom: calc(180rpx + env(safe-area-inset-bottom));
|
||||
right: 40rpx;
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
width: 112rpx;
|
||||
height: 112rpx;
|
||||
border-radius: 50%;
|
||||
background: #07c160;
|
||||
box-shadow: 0 10rpx 30rpx rgba(7, 193, 96, 0.3);
|
||||
background: #10b981;
|
||||
box-shadow: 0 10rpx 30rpx rgba(16, 185, 129, 0.3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 99;
|
||||
transition: transform 0.2s;
|
||||
|
||||
&:active { transform: scale(0.95); }
|
||||
}
|
||||
svg {
|
||||
width: 52rpx;
|
||||
height: 52rpx;
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 100rpx;
|
||||
color: #999;
|
||||
gap: 20rpx;
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
>
|
||||
<wd-img
|
||||
v-if="item.from_user?.avatar"
|
||||
:src="item.from_user.avatar"
|
||||
:src="resolveImageUrl(item.from_user.avatar)"
|
||||
width="80rpx"
|
||||
height="80rpx"
|
||||
radius="8rpx"
|
||||
@@ -46,7 +46,7 @@
|
||||
<view v-if="item.moment" class="moment-thumb">
|
||||
<wd-img
|
||||
v-if="item.moment.media_urls"
|
||||
:src="parseMediaUrls(item.moment.media_urls)[0]"
|
||||
:src="resolveImageUrl(parseMediaUrls(item.moment.media_urls)[0])"
|
||||
width="80rpx"
|
||||
height="80rpx"
|
||||
mode="aspectFill"
|
||||
@@ -68,6 +68,7 @@
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useMomentStore } from '@/stores'
|
||||
import { formatTime, generateColor } from '@/utils/format'
|
||||
import { resolveImageUrl } from '@/utils/image'
|
||||
import { parseMediaUrls } from '@/types/moment'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { useTheme } from '@/composables/useTheme'
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
<template>
|
||||
<wd-config-provider :theme="isDark ? 'dark' : 'light'">
|
||||
<view class="edit-page" :class="{ dark: isDark }">
|
||||
<app-nav-bar title="编辑资料" custom-style="background: transparent;" />
|
||||
<!-- 导航栏 -->
|
||||
<view class="nav-bar">
|
||||
<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>
|
||||
<text class="nav-title">{{ pageTitle }}</text>
|
||||
<view class="nav-placeholder"></view>
|
||||
</view>
|
||||
|
||||
<view class="content-body">
|
||||
<view class="input-card">
|
||||
<!-- 输入卡片 -->
|
||||
<view class="input-card animate-fade-in-up">
|
||||
<view class="input-label">{{ inputLabel }}</view>
|
||||
<textarea
|
||||
v-if="field === 'desc'"
|
||||
v-model="value"
|
||||
@@ -24,8 +35,11 @@
|
||||
<text v-if="maxLength" class="counter">{{ value.length }}/{{ maxLength }}</text>
|
||||
</view>
|
||||
|
||||
<view class="btn-wrap">
|
||||
<wd-button type="primary" block size="large" custom-style="border-radius: 24rpx; background: #4f46e5;" @click="save">保存</wd-button>
|
||||
<!-- 保存按钮 -->
|
||||
<view class="btn-wrap animate-fade-in-up" style="animation-delay: 100ms;">
|
||||
<view class="save-btn" @click="save">
|
||||
保存
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -36,59 +50,217 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useTheme } from '@/composables/useTheme'
|
||||
|
||||
// --- 逻辑不变 ---
|
||||
const { isDark } = useTheme(); const field = ref(''); const value = ref(''); const placeholder = ref(''); const maxLength = ref(0);
|
||||
const { isDark } = useTheme()
|
||||
const field = ref('')
|
||||
const value = ref('')
|
||||
const placeholder = ref('')
|
||||
const maxLength = ref(0)
|
||||
const pageTitle = ref('编辑资料')
|
||||
const inputLabel = ref('')
|
||||
|
||||
function goBack() { uni.navigateBack() }
|
||||
|
||||
onMounted(() => {
|
||||
const pages = getCurrentPages(); const currentPage = pages[pages.length - 1]; const options = (currentPage as any).$page?.options || {};
|
||||
field.value = options.field || ''; value.value = decodeURIComponent(options.value || '');
|
||||
const pages = getCurrentPages()
|
||||
const currentPage = pages[pages.length - 1]
|
||||
const options = (currentPage as any).$page?.options || {}
|
||||
field.value = options.field || ''
|
||||
value.value = decodeURIComponent(options.value || '')
|
||||
|
||||
switch (field.value) {
|
||||
case 'name': placeholder.value = '请输入昵称'; maxLength.value = 20; uni.setNavigationBarTitle({ title: '修改昵称' }); break;
|
||||
case 'desc': placeholder.value = '请输入个性签名'; maxLength.value = 100; uni.setNavigationBarTitle({ title: '修改签名' }); break;
|
||||
case 'region': placeholder.value = '请输入地区'; maxLength.value = 50; uni.setNavigationBarTitle({ title: '修改地区' }); break;
|
||||
case 'name':
|
||||
placeholder.value = '请输入昵称'
|
||||
maxLength.value = 20
|
||||
pageTitle.value = '修改昵称'
|
||||
inputLabel.value = '昵称'
|
||||
break
|
||||
case 'desc':
|
||||
placeholder.value = '请输入个性签名'
|
||||
maxLength.value = 100
|
||||
pageTitle.value = '修改签名'
|
||||
inputLabel.value = '个性签名'
|
||||
break
|
||||
case 'region':
|
||||
placeholder.value = '请输入地区'
|
||||
maxLength.value = 50
|
||||
pageTitle.value = '修改地区'
|
||||
inputLabel.value = '地区'
|
||||
break
|
||||
}
|
||||
})
|
||||
function save() { if (!value.value.trim()) { uni.showToast({ title: '内容不能为空', icon: 'none' }); return } uni.showToast({ title: '保存成功', icon: 'success' }); setTimeout(() => { uni.navigateBack() }, 1500) }
|
||||
|
||||
function save() {
|
||||
if (!value.value.trim()) {
|
||||
uni.showToast({ title: '内容不能为空', icon: 'none' })
|
||||
return
|
||||
}
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
setTimeout(() => { uni.navigateBack() }, 1500)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// 页面容器 - 浅色模式
|
||||
.edit-page {
|
||||
--bg-page: #f5f7fa;
|
||||
--bg-page: #f7f8fa;
|
||||
--bg-surface: #ffffff;
|
||||
--text-primary: #1d1d1f;
|
||||
--text-secondary: #6b7280;
|
||||
--text-tertiary: #9ca3af;
|
||||
--color-brand: #4F46E5;
|
||||
|
||||
min-height: 100vh;
|
||||
background: var(--bg-page);
|
||||
}
|
||||
|
||||
// 深色模式 - Warm Stone
|
||||
.edit-page.dark {
|
||||
--bg-page: #1c1c1e;
|
||||
--bg-surface: #2c2c2e;
|
||||
--text-primary: #f2f2f7;
|
||||
--bg-page: #1c1917;
|
||||
--bg-surface: #292524;
|
||||
--text-primary: #f5f5f4;
|
||||
--text-secondary: #e7e5e4;
|
||||
--text-tertiary: #78716c;
|
||||
--color-brand: #f97316;
|
||||
}
|
||||
|
||||
.content-body { padding: 40rpx; }
|
||||
// 动画
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20rpx);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in-up {
|
||||
animation: fadeInUp 0.4s ease-out forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
// 导航栏
|
||||
.nav-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: calc(var(--status-bar-height) + 16rpx) 24rpx 16rpx;
|
||||
background: var(--bg-page);
|
||||
|
||||
.nav-back {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: all 0.15s;
|
||||
|
||||
svg {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.nav-placeholder {
|
||||
width: 72rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// 内容
|
||||
.content-body {
|
||||
padding: 40rpx;
|
||||
}
|
||||
|
||||
// 输入卡片
|
||||
.input-card {
|
||||
background: var(--bg-surface);
|
||||
border-radius: 24rpx;
|
||||
padding: 30rpx;
|
||||
padding: 32rpx;
|
||||
position: relative;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.02);
|
||||
margin-bottom: 60rpx;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.03);
|
||||
margin-bottom: 48rpx;
|
||||
|
||||
.input-label {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-input {
|
||||
width: 100%; font-size: 32rpx; color: var(--text-primary);
|
||||
&.textarea { height: 200rpx; }
|
||||
width: 100%;
|
||||
font-size: 32rpx;
|
||||
color: var(--text-primary);
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 16rpx 0;
|
||||
border-bottom: 2rpx solid transparent;
|
||||
transition: border-color 0.2s;
|
||||
|
||||
&:focus {
|
||||
border-bottom-color: var(--color-brand);
|
||||
}
|
||||
|
||||
&.textarea {
|
||||
height: 200rpx;
|
||||
line-height: 1.6;
|
||||
resize: none;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.p-holder) { color: #999; }
|
||||
:deep(.p-holder) {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.counter {
|
||||
position: absolute; bottom: 20rpx; right: 30rpx;
|
||||
font-size: 24rpx; color: #999;
|
||||
position: absolute;
|
||||
bottom: 20rpx;
|
||||
right: 32rpx;
|
||||
font-size: 24rpx;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
// 保存按钮
|
||||
.btn-wrap {
|
||||
margin-top: 40rpx;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
width: 100%;
|
||||
height: 100rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%);
|
||||
color: #fff;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
border-radius: 24rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(79, 70, 229, 0.25);
|
||||
transition: all 0.15s;
|
||||
|
||||
.dark & {
|
||||
background: linear-gradient(135deg, #fb923c 0%, #f97316 100%);
|
||||
box-shadow: 0 8rpx 24rpx rgba(249, 115, 22, 0.25);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,23 +5,35 @@
|
||||
<!-- 头部大卡片 -->
|
||||
<view class="profile-header">
|
||||
<view class="header-top">
|
||||
<text class="page-title">我的</text>
|
||||
<view class="back-btn" @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>
|
||||
<text class="page-title">个人资料</text>
|
||||
<view class="icon-btn" @click="uni.navigateTo({ url: '/pages/settings/index' })">
|
||||
<wd-icon name="setting" size="44rpx" color="var(--text-primary)" />
|
||||
<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="3"/>
|
||||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
||||
</svg>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="user-hero" @click="goProfileEdit">
|
||||
<app-avatar :src="userInfo.avatar" :name="userInfo.nickname" :size="160" radius="50%" custom-style="border: 6rpx solid var(--bg-surface);" />
|
||||
<view class="user-hero animate-fade-in-up" @click="goProfileEdit">
|
||||
<view class="avatar-wrap">
|
||||
<app-avatar :src="userInfo.avatar" :name="userInfo.nickname" :size="160" round custom-style="border: 6rpx solid var(--bg-surface);" />
|
||||
</view>
|
||||
<view class="hero-info">
|
||||
<text class="nickname">{{ userInfo.nickname || '未设置昵称' }}</text>
|
||||
<text class="account">账号: {{ userInfo.account || '--' }}</text>
|
||||
</view>
|
||||
<wd-icon name="arrow-right" color="#999" />
|
||||
<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 class="stats-row">
|
||||
<view class="stats-row animate-fade-in-up" style="animation-delay: 100ms;">
|
||||
<view class="stat-item">
|
||||
<text class="num">12</text>
|
||||
<text class="label">动态</text>
|
||||
@@ -39,26 +51,48 @@
|
||||
|
||||
<!-- 菜单列表 -->
|
||||
<view class="menu-container">
|
||||
<view class="menu-group">
|
||||
<view class="menu-group animate-fade-in-up" style="animation-delay: 150ms;">
|
||||
<view class="menu-item" @click="uni.navigateTo({ url: '/pages/moment/my' })">
|
||||
<view class="icon-box blue"><wd-icon name="moments" size="40rpx" color="#fff" /></view>
|
||||
<view class="icon-box indigo">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2">
|
||||
<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>
|
||||
<text class="menu-text">我的动态</text>
|
||||
<wd-icon name="arrow-right" color="var(--text-tertiary)" size="28rpx" />
|
||||
<svg class="menu-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="9 18 15 12 9 6"/>
|
||||
</svg>
|
||||
</view>
|
||||
<view class="menu-item" @click="uni.navigateTo({ url: '/pages/collection/index' })">
|
||||
<view class="icon-box yellow"><wd-icon name="star" size="40rpx" color="#fff" /></view>
|
||||
<view class="icon-box amber">
|
||||
<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>
|
||||
<text class="menu-text">我的收藏</text>
|
||||
<wd-icon name="arrow-right" color="var(--text-tertiary)" size="28rpx" />
|
||||
<svg class="menu-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="9 18 15 12 9 6"/>
|
||||
</svg>
|
||||
</view>
|
||||
<view class="menu-item" @click="uni.navigateTo({ url: '/pages/album/index' })">
|
||||
<view class="icon-box green"><wd-icon name="picture" size="40rpx" color="#fff" /></view>
|
||||
<view class="icon-box emerald">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2">
|
||||
<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>
|
||||
<text class="menu-text">相册</text>
|
||||
<wd-icon name="arrow-right" color="var(--text-tertiary)" size="28rpx" />
|
||||
<svg class="menu-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="9 18 15 12 9 6"/>
|
||||
</svg>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<app-tab-bar active="/pages/profile/index" />
|
||||
</view>
|
||||
</wd-config-provider>
|
||||
</template>
|
||||
@@ -67,73 +101,249 @@
|
||||
import { computed } from 'vue'
|
||||
import { useTheme } from '@/composables/useTheme'
|
||||
import { useAuthStore } from '@/stores'
|
||||
import AppTabBar from '@/components/common/AppTabBar.vue'
|
||||
import AppAvatar from '@/components/common/AppAvatar.vue'
|
||||
|
||||
const { isDark } = useTheme()
|
||||
const authStore = useAuthStore()
|
||||
const userInfo = computed(() => authStore.userInfo || {})
|
||||
function goBack() { uni.navigateBack() }
|
||||
function goProfileEdit() { uni.navigateTo({ url: '/pages/profile/edit' }) }
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// 页面容器 - 浅色模式
|
||||
.page-container {
|
||||
--bg-page: #f5f7fa;
|
||||
--bg-page: #f7f8fa;
|
||||
--bg-surface: #ffffff;
|
||||
--text-primary: #1d1d1f;
|
||||
--text-secondary: #86868b;
|
||||
--text-secondary: #6b7280;
|
||||
--text-tertiary: #9ca3af;
|
||||
--border-color: rgba(0, 0, 0, 0.05);
|
||||
--color-brand: #4F46E5;
|
||||
|
||||
min-height: 100vh;
|
||||
background: var(--bg-page);
|
||||
padding-bottom: 200rpx;
|
||||
}
|
||||
.page-container.dark {
|
||||
--bg-page: #1c1c1e; /* Dark Grey */
|
||||
--bg-surface: #2c2c2e;
|
||||
--text-primary: #f2f2f7;
|
||||
padding-bottom: 80rpx;
|
||||
}
|
||||
|
||||
// 深色模式 - Warm Stone
|
||||
.page-container.dark {
|
||||
--bg-page: #1c1917;
|
||||
--bg-surface: #292524;
|
||||
--text-primary: #f5f5f4;
|
||||
--text-secondary: #e7e5e4;
|
||||
--text-tertiary: #78716c;
|
||||
--border-color: rgba(255, 255, 255, 0.1);
|
||||
--color-brand: #f97316;
|
||||
}
|
||||
|
||||
// 动画
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20rpx);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in-up {
|
||||
animation: fadeInUp 0.4s ease-out forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
// 头部
|
||||
.profile-header {
|
||||
background: var(--bg-surface);
|
||||
padding: calc(var(--status-bar-height) + 20rpx) 40rpx 60rpx;
|
||||
border-bottom-left-radius: 40rpx;
|
||||
border-bottom-right-radius: 40rpx;
|
||||
box-shadow: 0 10rpx 30rpx rgba(0,0,0,0.03);
|
||||
padding: calc(var(--status-bar-height) + 24rpx) 40rpx 60rpx;
|
||||
border-bottom-left-radius: 80rpx;
|
||||
border-bottom-right-radius: 80rpx;
|
||||
box-shadow: 0 10rpx 40rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.header-top {
|
||||
display: flex; justify-content: space-between; align-items: center; margin-bottom: 60rpx;
|
||||
.page-title { font-size: 48rpx; font-weight: 800; color: var(--text-primary); }
|
||||
.icon-btn { opacity: 0.7; }
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 48rpx;
|
||||
|
||||
.back-btn {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: all 0.15s;
|
||||
|
||||
svg {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: var(--border-color);
|
||||
}
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: all 0.15s;
|
||||
|
||||
svg {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
color: var(--text-primary);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: var(--border-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.user-hero {
|
||||
display: flex; align-items: center; gap: 30rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
transition: all 0.15s;
|
||||
|
||||
&:active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.avatar-wrap {
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.hero-info {
|
||||
flex: 1; display: flex; flex-direction: column;
|
||||
.nickname { font-size: 40rpx; font-weight: 700; color: var(--text-primary); margin-bottom: 8rpx; }
|
||||
.account { font-size: 26rpx; color: var(--text-secondary); }
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.nickname {
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.account {
|
||||
font-size: 26rpx;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.chevron {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
// 数据统计
|
||||
.stats-row {
|
||||
display: flex; justify-content: space-around; margin-top: 60rpx;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin-top: 60rpx;
|
||||
padding-top: 40rpx;
|
||||
border-top: 1rpx solid var(--border-color);
|
||||
|
||||
.stat-item {
|
||||
display: flex; flex-direction: column; align-items: center;
|
||||
.num { font-size: 36rpx; font-weight: 700; color: var(--text-primary); }
|
||||
.label { font-size: 24rpx; color: var(--text-secondary); margin-top: 4rpx; }
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.num {
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu-container { padding: 40rpx; }
|
||||
.menu-group { background: var(--bg-surface); border-radius: 24rpx; padding: 10rpx 30rpx; }
|
||||
// 菜单
|
||||
.menu-container {
|
||||
padding: 40rpx;
|
||||
}
|
||||
|
||||
.menu-group {
|
||||
background: var(--bg-surface);
|
||||
border-radius: 24rpx;
|
||||
padding: 8rpx 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex; align-items: center; padding: 30rpx 0; border-bottom: 1rpx solid rgba(0,0,0,0.05);
|
||||
&:last-child { border-bottom: none; }
|
||||
.icon-box {
|
||||
width: 70rpx; height: 70rpx; border-radius: 16rpx; display: flex; align-items: center; justify-content: center; margin-right: 24rpx;
|
||||
&.blue { background: #4f46e5; } &.yellow { background: #f59e0b; } &.green { background: #10b981; }
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 28rpx 32rpx;
|
||||
transition: background 0.15s;
|
||||
|
||||
&:active {
|
||||
background: var(--border-color);
|
||||
}
|
||||
|
||||
.icon-box {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 24rpx;
|
||||
|
||||
svg {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
}
|
||||
|
||||
&.indigo {
|
||||
background: #4F46E5;
|
||||
}
|
||||
|
||||
&.amber {
|
||||
background: #f59e0b;
|
||||
}
|
||||
|
||||
&.emerald {
|
||||
background: #10b981;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-text {
|
||||
flex: 1;
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.menu-arrow {
|
||||
width: 28rpx;
|
||||
height: 28rpx;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
.menu-text { flex: 1; font-size: 30rpx; font-weight: 500; color: var(--text-primary); }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,24 +1,39 @@
|
||||
<template>
|
||||
<view class="search-page" :class="{ dark: isDark }">
|
||||
<!-- 搜索栏 (增加 safe-area-top) -->
|
||||
<view class="search-header safe-area-top">
|
||||
<wd-search
|
||||
v-model="keyword"
|
||||
placeholder="搜索"
|
||||
show-action
|
||||
focus
|
||||
@search="doSearch"
|
||||
@cancel="goBack"
|
||||
@clear="clearKeyword"
|
||||
custom-style="background: transparent;"
|
||||
/>
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-header">
|
||||
<view class="search-box">
|
||||
<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"/>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
||||
</svg>
|
||||
<input
|
||||
class="search-input"
|
||||
type="text"
|
||||
v-model="keyword"
|
||||
placeholder="搜索"
|
||||
focus
|
||||
@confirm="doSearch"
|
||||
/>
|
||||
<view v-if="keyword" class="clear-btn" @click="clearKeyword">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm5 13.59L15.59 17 12 13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z"/>
|
||||
</svg>
|
||||
</view>
|
||||
</view>
|
||||
<text class="cancel-btn" @click="goBack">取消</text>
|
||||
</view>
|
||||
|
||||
<!-- 搜索历史 -->
|
||||
<view v-if="!keyword && history.length > 0" class="history-section">
|
||||
<view v-if="!keyword && history.length > 0" class="history-section animate-fade-in-up">
|
||||
<view class="section-header">
|
||||
<text class="title">搜索历史</text>
|
||||
<text class="clear" @click="clearHistory">清空</text>
|
||||
<view class="clear" @click="clearHistory">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="3 6 5 6 21 6"/>
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
||||
</svg>
|
||||
</view>
|
||||
</view>
|
||||
<view class="history-tags">
|
||||
<view
|
||||
@@ -35,31 +50,40 @@
|
||||
<!-- 搜索结果 -->
|
||||
<view v-if="keyword" class="results-section">
|
||||
<view v-if="loading" class="loading-state">
|
||||
<wd-loading />
|
||||
<wd-loading :color="isDark ? '#f97316' : '#4F46E5'" />
|
||||
<text>搜索中...</text>
|
||||
</view>
|
||||
|
||||
<wd-status-tip v-else-if="results.length === 0 && searched" tip="未找到相关结果" />
|
||||
<view v-else-if="results.length === 0 && searched" class="empty-state">
|
||||
<svg class="empty-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<circle cx="11" cy="11" r="8"/>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
||||
</svg>
|
||||
<text class="empty-text">未找到相关结果</text>
|
||||
</view>
|
||||
|
||||
<template v-else-if="results.length > 0">
|
||||
<view class="result-list">
|
||||
<view
|
||||
v-for="item in results"
|
||||
v-for="(item, index) in results"
|
||||
:key="item.id"
|
||||
class="result-item"
|
||||
class="result-item animate-fade-in-up"
|
||||
:style="{ animationDelay: `${index * 50}ms` }"
|
||||
@click="goDetail(item)"
|
||||
>
|
||||
<app-avatar
|
||||
:src="item.avatar"
|
||||
:name="item.name"
|
||||
:size="88"
|
||||
radius="12rpx"
|
||||
radius="20rpx"
|
||||
/>
|
||||
<view class="result-info">
|
||||
<text class="name">{{ item.name }}</text>
|
||||
<text class="name" v-html="highlightKeyword(item.name)"></text>
|
||||
<text class="desc">{{ item.desc || '暂无签名' }}</text>
|
||||
</view>
|
||||
<wd-icon name="arrow-right" size="32rpx" color="var(--text-tertiary)" />
|
||||
<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>
|
||||
</template>
|
||||
@@ -85,11 +109,7 @@ const history = ref<string[]>([])
|
||||
// 加载搜索历史
|
||||
const savedHistory = uni.getStorageSync('search_history')
|
||||
if (savedHistory) {
|
||||
try {
|
||||
history.value = JSON.parse(savedHistory)
|
||||
} catch {
|
||||
history.value = []
|
||||
}
|
||||
try { history.value = JSON.parse(savedHistory) } catch { history.value = [] }
|
||||
}
|
||||
|
||||
// 监听关键词变化
|
||||
@@ -97,9 +117,7 @@ let searchTimer: number
|
||||
watch(keyword, (val) => {
|
||||
clearTimeout(searchTimer)
|
||||
if (val.trim()) {
|
||||
searchTimer = setTimeout(() => {
|
||||
doSearch()
|
||||
}, 500) as unknown as number
|
||||
searchTimer = setTimeout(() => { doSearch() }, 500) as unknown as number
|
||||
} else {
|
||||
results.value = []
|
||||
searched.value = false
|
||||
@@ -108,12 +126,10 @@ watch(keyword, (val) => {
|
||||
|
||||
async function doSearch() {
|
||||
if (!keyword.value.trim()) return
|
||||
|
||||
loading.value = true
|
||||
searched.value = true
|
||||
try {
|
||||
results.value = await contactApi.searchUsers(keyword.value.trim())
|
||||
// 保存搜索历史
|
||||
saveHistory(keyword.value.trim())
|
||||
} catch (error) {
|
||||
console.error('搜索失败:', error)
|
||||
@@ -124,13 +140,9 @@ async function doSearch() {
|
||||
|
||||
function saveHistory(word: string) {
|
||||
const index = history.value.indexOf(word)
|
||||
if (index > -1) {
|
||||
history.value.splice(index, 1)
|
||||
}
|
||||
if (index > -1) { history.value.splice(index, 1) }
|
||||
history.value.unshift(word)
|
||||
if (history.value.length > 10) {
|
||||
history.value = history.value.slice(0, 10)
|
||||
}
|
||||
if (history.value.length > 10) { history.value = history.value.slice(0, 10) }
|
||||
uni.setStorageSync('search_history', JSON.stringify(history.value))
|
||||
}
|
||||
|
||||
@@ -150,80 +162,259 @@ function clearHistory() {
|
||||
uni.removeStorageSync('search_history')
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
function goBack() { uni.navigateBack() }
|
||||
|
||||
function goDetail(item: User) {
|
||||
uni.navigateTo({ url: `/pages/contact/detail?id=${item.id}` })
|
||||
}
|
||||
|
||||
// 高亮匹配文字
|
||||
function highlightKeyword(text: string): string {
|
||||
if (!keyword.value || !text) return text
|
||||
const regex = new RegExp(`(${keyword.value})`, 'gi')
|
||||
return text.replace(regex, '<span class="highlight">$1</span>')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// 页面容器 - 浅色模式
|
||||
.search-page {
|
||||
--bg-page: #f7f8fa;
|
||||
--bg-surface: #ffffff;
|
||||
--text-primary: #1d1d1f;
|
||||
--text-secondary: #6b7280;
|
||||
--text-tertiary: #9ca3af;
|
||||
--search-bg: #f1f1f1;
|
||||
--color-brand: #4F46E5;
|
||||
|
||||
min-height: 100vh;
|
||||
background: var(--bg-page);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
// 深色模式 - Warm Stone
|
||||
.search-page.dark {
|
||||
--bg-page: #1c1917;
|
||||
--bg-surface: #292524;
|
||||
--text-primary: #f5f5f4;
|
||||
--text-secondary: #e7e5e4;
|
||||
--text-tertiary: #78716c;
|
||||
--search-bg: #292524;
|
||||
--color-brand: #f97316;
|
||||
}
|
||||
|
||||
// 动画
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20rpx);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in-up {
|
||||
animation: fadeInUp 0.4s ease-out forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
// 搜索头部
|
||||
.search-header {
|
||||
background: var(--bg-content);
|
||||
padding: 10rpx 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
padding: calc(var(--status-bar-height) + 16rpx) 24rpx 16rpx;
|
||||
background: var(--bg-page);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.safe-area-top {
|
||||
padding-top: var(--status-bar-height);
|
||||
.search-box {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--search-bg);
|
||||
padding: 0 24rpx;
|
||||
border-radius: 24rpx;
|
||||
height: 80rpx;
|
||||
|
||||
.search-icon {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
color: var(--text-tertiary);
|
||||
margin-right: 12rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
font-size: 30rpx;
|
||||
color: var(--text-primary);
|
||||
background: transparent;
|
||||
border: none;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-tertiary);
|
||||
|
||||
svg {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
}
|
||||
|
||||
&:active { opacity: 0.7; }
|
||||
}
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
font-size: 30rpx;
|
||||
color: var(--color-brand);
|
||||
padding: 10rpx;
|
||||
|
||||
&:active { opacity: 0.7; }
|
||||
}
|
||||
|
||||
// 历史记录
|
||||
.history-section {
|
||||
padding: 30rpx;
|
||||
padding: 32rpx;
|
||||
|
||||
.section-header {
|
||||
display: flex; justify-content: space-between; margin-bottom: 24rpx;
|
||||
.title { font-size: 28rpx; font-weight: 600; color: var(--text-primary); }
|
||||
.clear { font-size: 26rpx; color: var(--text-tertiary); }
|
||||
}
|
||||
.history-tags {
|
||||
display: flex; flex-wrap: wrap; gap: 20rpx;
|
||||
.history-tag {
|
||||
background: var(--bg-content); padding: 10rpx 24rpx; border-radius: 30rpx;
|
||||
font-size: 26rpx; color: var(--text-secondary);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.clear {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-tertiary);
|
||||
|
||||
svg {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
}
|
||||
|
||||
&:active { opacity: 0.7; }
|
||||
}
|
||||
}
|
||||
|
||||
.history-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
|
||||
.history-tag {
|
||||
background: var(--bg-surface);
|
||||
padding: 16rpx 28rpx;
|
||||
border-radius: 40rpx;
|
||||
font-size: 26rpx;
|
||||
color: var(--text-secondary);
|
||||
transition: all 0.15s;
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 结果区
|
||||
.results-section {
|
||||
flex: 1;
|
||||
background: var(--bg-page);
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
padding-top: 100rpx; color: var(--text-tertiary); gap: 20rpx;
|
||||
.loading-state, .empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: 120rpx;
|
||||
color: var(--text-tertiary);
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
// 结果列表
|
||||
.result-list {
|
||||
padding: 20rpx;
|
||||
padding: 20rpx 32rpx;
|
||||
}
|
||||
|
||||
.result-item {
|
||||
display: flex; align-items: center;
|
||||
background: var(--bg-content);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--bg-surface);
|
||||
padding: 24rpx;
|
||||
border-radius: 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
transition: opacity 0.2s;
|
||||
transition: all 0.15s;
|
||||
|
||||
&:active { opacity: 0.8; }
|
||||
&:active {
|
||||
transform: scale(0.99);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.result-info {
|
||||
flex: 1; margin-left: 24rpx; display: flex; flex-direction: column;
|
||||
.name { font-size: 32rpx; font-weight: 600; color: var(--text-primary); }
|
||||
.desc { font-size: 24rpx; color: var(--text-tertiary); margin-top: 6rpx; }
|
||||
flex: 1;
|
||||
margin-left: 24rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.name {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
|
||||
:deep(.highlight) {
|
||||
color: var(--color-brand);
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-tertiary);
|
||||
margin-top: 6rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.chevron {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,39 +1,134 @@
|
||||
<template>
|
||||
<wd-config-provider :theme="isDark ? 'dark' : 'light'">
|
||||
<view class="settings-page" :class="{ dark: isDark }">
|
||||
<app-nav-bar title="设置" custom-style="background: transparent;" />
|
||||
<!-- 导航栏 -->
|
||||
<view class="nav-bar">
|
||||
<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>
|
||||
<text class="nav-title">设置</text>
|
||||
<view class="nav-placeholder"></view>
|
||||
</view>
|
||||
|
||||
<view class="content-body">
|
||||
<!-- 账号安全 -->
|
||||
<view class="settings-group">
|
||||
<view class="settings-group animate-fade-in-up">
|
||||
<view class="group-title">账号</view>
|
||||
<view class="group-card">
|
||||
<wd-cell title="账号管理" is-link to="/pages/settings/account" size="large" />
|
||||
<wd-cell title="隐私设置" is-link to="/pages/settings/privacy" size="large" />
|
||||
<view class="setting-item" @click="goTo('/pages/settings/account')">
|
||||
<view class="icon-box blue">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2">
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
|
||||
</svg>
|
||||
</view>
|
||||
<text class="item-text">账号管理</text>
|
||||
<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 class="setting-item" @click="goTo('/pages/settings/privacy')">
|
||||
<view class="icon-box purple">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2">
|
||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
|
||||
</svg>
|
||||
</view>
|
||||
<text class="item-text">隐私设置</text>
|
||||
<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 class="settings-group">
|
||||
<view class="settings-group animate-fade-in-up" style="animation-delay: 50ms;">
|
||||
<view class="group-title">通用</view>
|
||||
<view class="group-card">
|
||||
<wd-cell title="新消息通知" is-link to="/pages/settings/notification" size="large" />
|
||||
<wd-cell title="主题设置" is-link to="/pages/settings/theme" value="自动" size="large" />
|
||||
<wd-cell title="通用设置" is-link to="/pages/settings/general" size="large" />
|
||||
<wd-cell title="清理缓存" is-link value="12.5MB" clickable size="large" @click="handleClearCache" />
|
||||
<view class="setting-item" @click="goTo('/pages/settings/notification')">
|
||||
<view class="icon-box red">
|
||||
<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"/>
|
||||
</svg>
|
||||
</view>
|
||||
<text class="item-text">新消息通知</text>
|
||||
<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 class="setting-item" @click="goTo('/pages/settings/theme')">
|
||||
<view class="icon-box indigo">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="5"/>
|
||||
<line x1="12" y1="1" x2="12" y2="3"/>
|
||||
<line x1="12" y1="21" x2="12" y2="23"/>
|
||||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
|
||||
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
|
||||
<line x1="1" y1="12" x2="3" y2="12"/>
|
||||
<line x1="21" y1="12" x2="23" y2="12"/>
|
||||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
|
||||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
|
||||
</svg>
|
||||
</view>
|
||||
<text class="item-text">主题设置</text>
|
||||
<text class="item-value">{{ themeText }}</text>
|
||||
<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 class="setting-item" @click="handleClearCache">
|
||||
<view class="icon-box green">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2">
|
||||
<polyline points="3 6 5 6 21 6"/>
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
||||
</svg>
|
||||
</view>
|
||||
<text class="item-text">清理缓存</text>
|
||||
<text class="item-value">{{ cacheSize }}</text>
|
||||
<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 class="settings-group">
|
||||
<view class="settings-group animate-fade-in-up" style="animation-delay: 100ms;">
|
||||
<view class="group-title">关于</view>
|
||||
<view class="group-card">
|
||||
<wd-cell title="帮助与反馈" is-link to="/pages/settings/feedback" size="large" />
|
||||
<wd-cell title="关于我们" is-link to="/pages/settings/about" value="v1.0.0" size="large" />
|
||||
<view class="setting-item" @click="goTo('/pages/settings/feedback')">
|
||||
<view class="icon-box orange">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" 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>
|
||||
</view>
|
||||
<text class="item-text">帮助与反馈</text>
|
||||
<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 class="setting-item" @click="goTo('/pages/settings/about')">
|
||||
<view class="icon-box gray">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="12" y1="16" x2="12" y2="12"/>
|
||||
<line x1="12" y1="8" x2="12.01" y2="8"/>
|
||||
</svg>
|
||||
</view>
|
||||
<text class="item-text">关于我们</text>
|
||||
<text class="item-value">v1.0.0</text>
|
||||
<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 class="logout-btn" @click="handleLogout">
|
||||
<view class="logout-btn animate-fade-in-up" style="animation-delay: 150ms;" @click="handleLogout">
|
||||
<text>退出登录</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -45,51 +140,230 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useTheme } from '@/composables/useTheme'
|
||||
import { useAuthStore } from '@/stores'
|
||||
import { useToast, useMessage } from 'wot-design-uni'
|
||||
import AppNavBar from '@/components/common/AppNavBar.vue'
|
||||
|
||||
// --- 逻辑不变 ---
|
||||
const { isDark } = useTheme(); const authStore = useAuthStore(); const toast = useToast(); const message = useMessage();
|
||||
function handleLogout() { message.confirm({ title: '提示', msg: '确定要退出登录吗?', success: async () => { try { await authStore.logout(); uni.reLaunch({ url: '/pages/login/index' }) } catch (error) { toast.error('退出失败') } } }) }
|
||||
function handleClearCache() { toast.loading('清理中...'); setTimeout(() => { toast.success('清理完成') }, 1000) }
|
||||
const { isDark } = useTheme()
|
||||
const authStore = useAuthStore()
|
||||
const toast = useToast()
|
||||
const message = useMessage()
|
||||
const cacheSize = ref('12.5MB')
|
||||
|
||||
const themeText = computed(() => {
|
||||
const mode = uni.getStorageSync('nl_im_theme_mode') || 'system'
|
||||
const texts: Record<string, string> = { system: '跟随系统', light: '浅色', dark: '深色' }
|
||||
return texts[mode] || '跟随系统'
|
||||
})
|
||||
|
||||
function goBack() { uni.navigateBack() }
|
||||
function goTo(url: string) { uni.navigateTo({ url }) }
|
||||
|
||||
function handleLogout() {
|
||||
message.confirm({
|
||||
title: '提示',
|
||||
msg: '确定要退出登录吗?',
|
||||
success: async () => {
|
||||
try {
|
||||
await authStore.logout()
|
||||
uni.reLaunch({ url: '/pages/login/index' })
|
||||
} catch (error) {
|
||||
toast.error('退出失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleClearCache() {
|
||||
toast.loading('清理中...')
|
||||
setTimeout(() => {
|
||||
cacheSize.value = '0KB'
|
||||
toast.success('清理完成')
|
||||
}, 1000)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// 页面容器 - 浅色模式
|
||||
.settings-page {
|
||||
--bg-page: #f5f7fa;
|
||||
--bg-page: #f7f8fa;
|
||||
--bg-surface: #ffffff;
|
||||
--text-secondary: #86868b;
|
||||
--text-primary: #1d1d1f;
|
||||
--text-secondary: #6b7280;
|
||||
--text-tertiary: #9ca3af;
|
||||
--border-color: rgba(0, 0, 0, 0.05);
|
||||
|
||||
min-height: 100vh;
|
||||
background: var(--bg-page);
|
||||
}
|
||||
|
||||
// 深色模式 - Warm Stone
|
||||
.settings-page.dark {
|
||||
--bg-page: #1c1c1e;
|
||||
--bg-surface: #2c2c2e;
|
||||
--text-secondary: #aeaeb2;
|
||||
--bg-page: #1c1917;
|
||||
--bg-surface: #292524;
|
||||
--text-primary: #f5f5f4;
|
||||
--text-secondary: #e7e5e4;
|
||||
--text-tertiary: #78716c;
|
||||
--border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.content-body { padding: 30rpx; }
|
||||
// 动画
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20rpx);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in-up {
|
||||
animation: fadeInUp 0.4s ease-out forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
// 导航栏
|
||||
.nav-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: calc(var(--status-bar-height) + 16rpx) 24rpx 16rpx;
|
||||
background: var(--bg-page);
|
||||
|
||||
.nav-back {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: all 0.15s;
|
||||
|
||||
svg {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: var(--border-color);
|
||||
}
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.nav-placeholder {
|
||||
width: 72rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// 内容
|
||||
.content-body {
|
||||
padding: 20rpx 32rpx;
|
||||
}
|
||||
|
||||
// 设置组
|
||||
.settings-group {
|
||||
margin-bottom: 40rpx;
|
||||
|
||||
.group-title {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 16rpx;
|
||||
margin-left: 12rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-group { margin-bottom: 40rpx; }
|
||||
.group-title { font-size: 26rpx; color: var(--text-secondary); margin-bottom: 16rpx; margin-left: 20rpx; }
|
||||
.group-card {
|
||||
border-radius: 24rpx; overflow: hidden;
|
||||
/* 覆盖 Cell 背景 */
|
||||
:deep(.wd-cell) { background-color: var(--bg-surface); }
|
||||
:deep(.wd-cell__title) { font-weight: 500; }
|
||||
background: var(--bg-surface);
|
||||
border-radius: 24rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 24rpx 28rpx;
|
||||
border-bottom: 1rpx solid var(--border-color);
|
||||
transition: background 0.15s;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: var(--border-color);
|
||||
}
|
||||
|
||||
.icon-box {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 24rpx;
|
||||
|
||||
svg {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
}
|
||||
|
||||
&.blue { background: #0ea5e9; }
|
||||
&.purple { background: #8b5cf6; }
|
||||
&.red { background: #ef4444; }
|
||||
&.indigo { background: #4F46E5; }
|
||||
&.green { background: #10b981; }
|
||||
&.orange { background: #f97316; }
|
||||
&.gray { background: #6b7280; }
|
||||
}
|
||||
|
||||
.item-text {
|
||||
flex: 1;
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.item-value {
|
||||
font-size: 26rpx;
|
||||
color: var(--text-tertiary);
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
|
||||
.chevron {
|
||||
width: 28rpx;
|
||||
height: 28rpx;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
// 退出按钮
|
||||
.logout-btn {
|
||||
background: var(--bg-surface);
|
||||
border-radius: 24rpx;
|
||||
padding: 30rpx;
|
||||
padding: 32rpx;
|
||||
text-align: center;
|
||||
margin-top: 60rpx;
|
||||
text { color: #ef4444; font-size: 32rpx; font-weight: 600; }
|
||||
&:active { opacity: 0.8; background: rgba(0,0,0,0.02); }
|
||||
margin-top: 40rpx;
|
||||
transition: all 0.15s;
|
||||
|
||||
text {
|
||||
color: #ef4444;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.8;
|
||||
transform: scale(0.99);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,37 +1,81 @@
|
||||
<template>
|
||||
<wd-config-provider :theme="isDark ? 'dark' : 'light'">
|
||||
<view class="theme-page" :class="{ dark: isDark }">
|
||||
<app-nav-bar title="主题设置" custom-style="background: transparent;" />
|
||||
<!-- 导航栏 -->
|
||||
<view class="nav-bar">
|
||||
<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>
|
||||
<text class="nav-title">主题设置</text>
|
||||
<view class="nav-placeholder"></view>
|
||||
</view>
|
||||
|
||||
<view class="content-body">
|
||||
<view class="option-list">
|
||||
<!-- 浅色 -->
|
||||
<view class="option-card" :class="{ active: currentTheme === 'light' }" @click="selectTheme('light')">
|
||||
<view class="preview light-prev">
|
||||
<view class="p-nav"></view><view class="p-body"></view>
|
||||
<!-- 主题选项网格 -->
|
||||
<view class="option-grid">
|
||||
<!-- 浅色模式 -->
|
||||
<view class="option-card animate-fade-in-up" :class="{ active: currentTheme === 'light' }" @click="selectTheme('light')">
|
||||
<view class="phone-preview light-preview">
|
||||
<view class="preview-nav"></view>
|
||||
<view class="preview-body">
|
||||
<view class="preview-card"></view>
|
||||
<view class="preview-card short"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="option-info">
|
||||
<text class="option-label">浅色模式</text>
|
||||
<view v-if="currentTheme === 'light'" class="check-mark">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="3">
|
||||
<polyline points="20 6 9 17 4 12"/>
|
||||
</svg>
|
||||
</view>
|
||||
</view>
|
||||
<text class="label">浅色模式</text>
|
||||
<wd-icon v-if="currentTheme === 'light'" name="check" color="#4f46e5" class="check-icon" />
|
||||
</view>
|
||||
|
||||
<!-- 深色 -->
|
||||
<view class="option-card" :class="{ active: currentTheme === 'dark' }" @click="selectTheme('dark')">
|
||||
<view class="preview dark-prev">
|
||||
<view class="p-nav"></view><view class="p-body"></view>
|
||||
<!-- 深色模式 -->
|
||||
<view class="option-card animate-fade-in-up" style="animation-delay: 50ms;" :class="{ active: currentTheme === 'dark' }" @click="selectTheme('dark')">
|
||||
<view class="phone-preview dark-preview">
|
||||
<view class="preview-nav"></view>
|
||||
<view class="preview-body">
|
||||
<view class="preview-card"></view>
|
||||
<view class="preview-card short"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="option-info">
|
||||
<text class="option-label">深色模式</text>
|
||||
<view v-if="currentTheme === 'dark'" class="check-mark">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="3">
|
||||
<polyline points="20 6 9 17 4 12"/>
|
||||
</svg>
|
||||
</view>
|
||||
</view>
|
||||
<text class="label">深色模式</text>
|
||||
<wd-icon v-if="currentTheme === 'dark'" name="check" color="#4f46e5" class="check-icon" />
|
||||
</view>
|
||||
|
||||
<!-- 跟随系统 -->
|
||||
<view class="option-card system" :class="{ active: currentTheme === 'system' }" @click="selectTheme('system')">
|
||||
<view class="system-icon"><wd-icon name="setting" size="48rpx" /></view>
|
||||
<text class="label">跟随系统</text>
|
||||
<wd-icon v-if="currentTheme === 'system'" name="check" color="#4f46e5" class="check-icon" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="tips">
|
||||
<!-- 跟随系统选项 -->
|
||||
<view class="system-option animate-fade-in-up" style="animation-delay: 100ms;" :class="{ active: currentTheme === 'system' }" @click="selectTheme('system')">
|
||||
<view class="system-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/>
|
||||
<line x1="8" y1="21" x2="16" y2="21"/>
|
||||
<line x1="12" y1="17" x2="12" y2="21"/>
|
||||
</svg>
|
||||
</view>
|
||||
<view class="system-info">
|
||||
<text class="system-label">跟随系统</text>
|
||||
<text class="system-desc">根据系统设置自动切换</text>
|
||||
</view>
|
||||
<view v-if="currentTheme === 'system'" class="check-mark">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="3">
|
||||
<polyline points="20 6 9 17 4 12"/>
|
||||
</svg>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 提示 -->
|
||||
<view class="tips animate-fade-in-up" style="animation-delay: 150ms;">
|
||||
<text>「跟随系统」开启后,将根据系统显示设置自动切换深色或浅色模式。</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -46,70 +90,297 @@ import { ref, onMounted } from 'vue'
|
||||
import { useTheme } from '@/composables/useTheme'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
|
||||
// --- 逻辑不变 ---
|
||||
const { isDark, setTheme: applyTheme } = useTheme(); const toast = useToast(); const currentTheme = ref<'system' | 'light' | 'dark'>('system');
|
||||
onMounted(() => { const saved = uni.getStorageSync('nl_im_theme_mode'); if (saved === 'light' || saved === 'dark' || saved === 'system') { currentTheme.value = saved } });
|
||||
const { isDark, setTheme: applyTheme } = useTheme()
|
||||
const toast = useToast()
|
||||
const currentTheme = ref<'system' | 'light' | 'dark'>('system')
|
||||
|
||||
function goBack() { uni.navigateBack() }
|
||||
|
||||
onMounted(() => {
|
||||
const saved = uni.getStorageSync('nl_im_theme_mode')
|
||||
if (saved === 'light' || saved === 'dark' || saved === 'system') {
|
||||
currentTheme.value = saved
|
||||
}
|
||||
})
|
||||
|
||||
function selectTheme(mode: 'system' | 'light' | 'dark') {
|
||||
currentTheme.value = mode; uni.setStorageSync('nl_im_theme_mode', mode);
|
||||
if (mode === 'system') { uni.removeStorageSync('nl_im_theme'); let systemDark = false; try { systemDark = uni.getSystemInfoSync().theme === 'dark' } catch (e) {} applyTheme(systemDark, false) }
|
||||
else { applyTheme(mode === 'dark', true) }
|
||||
const tips = { system: '已设置为跟随系统', light: '已切换为浅色模式', dark: '已切换为深色模式' }; toast.success(tips[mode])
|
||||
currentTheme.value = mode
|
||||
uni.setStorageSync('nl_im_theme_mode', mode)
|
||||
|
||||
if (mode === 'system') {
|
||||
uni.removeStorageSync('nl_im_theme')
|
||||
let systemDark = false
|
||||
try { systemDark = uni.getSystemInfoSync().theme === 'dark' } catch (e) {}
|
||||
applyTheme(systemDark, false)
|
||||
} else {
|
||||
applyTheme(mode === 'dark', true)
|
||||
}
|
||||
|
||||
const tips = { system: '已设置为跟随系统', light: '已切换为浅色模式', dark: '已切换为深色模式' }
|
||||
toast.success(tips[mode])
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// 页面容器 - 浅色模式
|
||||
.theme-page {
|
||||
--bg-page: #f5f7fa;
|
||||
--bg-page: #f7f8fa;
|
||||
--bg-surface: #ffffff;
|
||||
--text-primary: #1d1d1f;
|
||||
--text-secondary: #6b7280;
|
||||
--text-tertiary: #9ca3af;
|
||||
--border-color: rgba(0, 0, 0, 0.05);
|
||||
--color-brand: #4F46E5;
|
||||
|
||||
min-height: 100vh;
|
||||
background: var(--bg-page);
|
||||
}
|
||||
|
||||
// 深色模式 - Warm Stone
|
||||
.theme-page.dark {
|
||||
--bg-page: #1c1c1e;
|
||||
--bg-surface: #2c2c2e;
|
||||
--text-primary: #f2f2f7;
|
||||
--bg-page: #1c1917;
|
||||
--bg-surface: #292524;
|
||||
--text-primary: #f5f5f4;
|
||||
--text-secondary: #e7e5e4;
|
||||
--text-tertiary: #78716c;
|
||||
--border-color: rgba(255, 255, 255, 0.1);
|
||||
--color-brand: #f97316;
|
||||
}
|
||||
|
||||
.content-body { padding: 40rpx; }
|
||||
// 动画
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20rpx);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.option-list {
|
||||
display: grid; grid-template-columns: 1fr 1fr; gap: 30rpx; margin-bottom: 40rpx;
|
||||
.animate-fade-in-up {
|
||||
animation: fadeInUp 0.4s ease-out forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
// 导航栏
|
||||
.nav-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: calc(var(--status-bar-height) + 16rpx) 24rpx 16rpx;
|
||||
background: var(--bg-page);
|
||||
|
||||
.nav-back {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: all 0.15s;
|
||||
|
||||
svg {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: var(--border-color);
|
||||
}
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.nav-placeholder {
|
||||
width: 72rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// 内容
|
||||
.content-body {
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
// 选项网格
|
||||
.option-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.option-card {
|
||||
background: var(--bg-surface); border-radius: 24rpx; padding: 30rpx;
|
||||
display: flex; flex-direction: column; align-items: center; gap: 20rpx;
|
||||
border: 4rpx solid transparent; position: relative;
|
||||
background: var(--bg-surface);
|
||||
border-radius: 24rpx;
|
||||
padding: 24rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
border: 4rpx solid transparent;
|
||||
transition: all 0.2s;
|
||||
|
||||
&.active { border-color: #4f46e5; background: rgba(79, 70, 229, 0.05); }
|
||||
&.active {
|
||||
border-color: var(--color-brand);
|
||||
}
|
||||
|
||||
.label { font-size: 28rpx; font-weight: 600; color: var(--text-primary); }
|
||||
.check-icon { position: absolute; top: 20rpx; right: 20rpx; }
|
||||
|
||||
&.system { grid-column: span 2; flex-direction: row; padding: 40rpx; }
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
/* 简单的 CSS 预览图 */
|
||||
.preview {
|
||||
width: 120rpx; height: 160rpx; border-radius: 12rpx; border: 2rpx solid rgba(0,0,0,0.1); overflow: hidden; display: flex; flex-direction: column;
|
||||
.p-nav { height: 30rpx; border-bottom: 1rpx solid rgba(0,0,0,0.05); }
|
||||
.p-body { flex: 1; margin: 10rpx; border-radius: 8rpx; }
|
||||
// 手机预览
|
||||
.phone-preview {
|
||||
width: 160rpx;
|
||||
height: 220rpx;
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 2rpx solid var(--border-color);
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
.preview-nav {
|
||||
height: 40rpx;
|
||||
border-bottom: 1rpx solid var(--border-color);
|
||||
}
|
||||
|
||||
.preview-body {
|
||||
flex: 1;
|
||||
padding: 16rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.preview-card {
|
||||
height: 50rpx;
|
||||
border-radius: 8rpx;
|
||||
|
||||
&.short {
|
||||
width: 70%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.light-prev {
|
||||
|
||||
.light-preview {
|
||||
background: #fff;
|
||||
.p-nav { background: #f7f7f7; } .p-body { background: #eee; }
|
||||
|
||||
.preview-nav {
|
||||
background: #f7f7f7;
|
||||
}
|
||||
|
||||
.preview-card {
|
||||
background: #e5e7eb;
|
||||
}
|
||||
}
|
||||
.dark-prev {
|
||||
background: #1c1c1e; border-color: #333;
|
||||
.p-nav { background: #2c2c2e; border-color: #333; } .p-body { background: #333; }
|
||||
|
||||
.dark-preview {
|
||||
background: #1c1917;
|
||||
border-color: #44403c;
|
||||
|
||||
.preview-nav {
|
||||
background: #292524;
|
||||
border-color: #44403c;
|
||||
}
|
||||
|
||||
.preview-card {
|
||||
background: #44403c;
|
||||
}
|
||||
}
|
||||
|
||||
.option-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.option-label {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.check-mark {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
background: var(--color-brand);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
svg {
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// 跟随系统
|
||||
.system-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--bg-surface);
|
||||
border-radius: 24rpx;
|
||||
padding: 32rpx;
|
||||
border: 4rpx solid transparent;
|
||||
transition: all 0.2s;
|
||||
|
||||
&.active {
|
||||
border-color: var(--color-brand);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.99);
|
||||
}
|
||||
}
|
||||
|
||||
.system-icon {
|
||||
width: 80rpx; height: 80rpx; background: rgba(0,0,0,0.05); border-radius: 50%;
|
||||
display: flex; align-items: center; justify-content: center; color: var(--text-primary); margin-right: 20rpx;
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
background: var(--border-color);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 24rpx;
|
||||
|
||||
svg {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.tips { padding: 0 10rpx; font-size: 24rpx; color: #999; line-height: 1.5; }
|
||||
.system-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.system-label {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.system-desc {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-tertiary);
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
// 提示
|
||||
.tips {
|
||||
padding: 32rpx 12rpx;
|
||||
font-size: 24rpx;
|
||||
color: var(--text-tertiary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,3 +5,9 @@ placeholder
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -5,3 +5,9 @@ placeholder
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -5,3 +5,9 @@ placeholder
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -5,3 +5,9 @@ placeholder
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -5,3 +5,9 @@ placeholder
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -5,3 +5,9 @@ placeholder
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -5,3 +5,9 @@ placeholder
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,191 @@
|
||||
/**
|
||||
* 公共样式
|
||||
* 包含布局、动画、工具类
|
||||
*/
|
||||
|
||||
// ==========================================
|
||||
// 关键帧动画
|
||||
// ==========================================
|
||||
|
||||
// 淡入上滑动画
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20rpx);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
// 弹入动画
|
||||
@keyframes popIn {
|
||||
from {
|
||||
transform: scale(0.95);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 滑入动画
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
// 滑出动画
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
transform: translateY(0);
|
||||
}
|
||||
to {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
}
|
||||
|
||||
// 波形动画 (语音消息)
|
||||
@keyframes wave {
|
||||
0%, 100% {
|
||||
height: 8rpx;
|
||||
}
|
||||
50% {
|
||||
height: 32rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// 脉冲动画 (通话指示器)
|
||||
@keyframes pulse-ring {
|
||||
0% {
|
||||
transform: scale(0.8);
|
||||
opacity: 0.5;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1.3);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 涟漪动画 (来电头像)
|
||||
@keyframes ripple {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 0.4;
|
||||
}
|
||||
100% {
|
||||
transform: scale(2.5);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 呼吸动画
|
||||
@keyframes breathing {
|
||||
0%, 100% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 弹跳动画
|
||||
@keyframes bounce {
|
||||
0%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-4rpx);
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 过渡和动画工具类
|
||||
// ==========================================
|
||||
|
||||
.smooth-transition {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.fast-transition {
|
||||
transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.animate-fade-in-up {
|
||||
animation: fadeInUp 0.4s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-pop-in {
|
||||
animation: popIn 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
|
||||
}
|
||||
|
||||
.animate-slide-up {
|
||||
animation: slideUp 0.3s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-pulse {
|
||||
animation: breathing 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
// 动画延迟
|
||||
.delay-50 { animation-delay: 50ms; }
|
||||
.delay-100 { animation-delay: 100ms; }
|
||||
.delay-150 { animation-delay: 150ms; }
|
||||
.delay-200 { animation-delay: 200ms; }
|
||||
.delay-300 { animation-delay: 300ms; }
|
||||
|
||||
// ==========================================
|
||||
// 滚动条隐藏
|
||||
// ==========================================
|
||||
|
||||
.custom-scrollbar {
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 毛玻璃效果
|
||||
// ==========================================
|
||||
|
||||
.glass-effect {
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
|
||||
.dark & {
|
||||
background: rgba(28, 25, 23, 0.85);
|
||||
}
|
||||
}
|
||||
|
||||
.glass-light {
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 安全区域
|
||||
// ==========================================
|
||||
|
||||
:root {
|
||||
// 状态栏高度 - UniApp 会自动注入
|
||||
--status-bar-height: var(--status-bar-height, 44px);
|
||||
// 安全区域
|
||||
--safe-area-top: env(safe-area-inset-top, 0px);
|
||||
--safe-area-bottom: env(safe-area-inset-bottom, 0px);
|
||||
--safe-area-left: env(safe-area-inset-left, 0px);
|
||||
--safe-area-right: env(safe-area-inset-right, 0px);
|
||||
}
|
||||
|
||||
.safe-area-top {
|
||||
padding-top: var(--status-bar-height, 0);
|
||||
}
|
||||
@@ -11,7 +194,28 @@
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
// 安全区域占位
|
||||
.safe-area-spacer {
|
||||
height: calc(160rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
// 响应式断点
|
||||
@mixin respond-to($breakpoint) {
|
||||
@if $breakpoint == 'sm' {
|
||||
@media (max-width: 375px) { @content; }
|
||||
}
|
||||
@else if $breakpoint == 'md' {
|
||||
@media (min-width: 376px) and (max-width: 414px) { @content; }
|
||||
}
|
||||
@else if $breakpoint == 'lg' {
|
||||
@media (min-width: 415px) { @content; }
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Flex 布局
|
||||
// ==========================================
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
@@ -37,7 +241,38 @@
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.flex-shrink-0 {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.items-start {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.items-end {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.justify-end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 文本
|
||||
// ==========================================
|
||||
|
||||
.text-ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -64,7 +299,18 @@
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.font-medium {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 边距
|
||||
// ==========================================
|
||||
|
||||
.m-0 { margin: 0; }
|
||||
.mt-10 { margin-top: 10rpx; }
|
||||
.mt-20 { margin-top: 20rpx; }
|
||||
@@ -72,33 +318,82 @@
|
||||
.mb-10 { margin-bottom: 10rpx; }
|
||||
.mb-20 { margin-bottom: 20rpx; }
|
||||
.mb-30 { margin-bottom: 30rpx; }
|
||||
.ml-10 { margin-left: 10rpx; }
|
||||
.mr-10 { margin-right: 10rpx; }
|
||||
|
||||
.p-0 { padding: 0; }
|
||||
.p-20 { padding: 20rpx; }
|
||||
.p-30 { padding: 30rpx; }
|
||||
.px-20 { padding-left: 20rpx; padding-right: 20rpx; }
|
||||
.px-30 { padding-left: 30rpx; padding-right: 30rpx; }
|
||||
.py-20 { padding-top: 20rpx; padding-bottom: 20rpx; }
|
||||
|
||||
// ==========================================
|
||||
// 圆角规范
|
||||
// ==========================================
|
||||
|
||||
.rounded-sm { border-radius: 8rpx; }
|
||||
.rounded { border-radius: 16rpx; }
|
||||
.rounded-lg { border-radius: 20rpx; }
|
||||
.rounded-xl { border-radius: 24rpx; }
|
||||
.rounded-2xl { border-radius: 32rpx; }
|
||||
.rounded-3xl { border-radius: 40rpx; }
|
||||
.rounded-full { border-radius: 50%; }
|
||||
|
||||
// ==========================================
|
||||
// 阴影规范
|
||||
// ==========================================
|
||||
|
||||
.shadow-sm {
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.shadow {
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.shadow-lg {
|
||||
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.shadow-xl {
|
||||
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 页面容器
|
||||
// ==========================================
|
||||
|
||||
.page-container {
|
||||
min-height: 100vh;
|
||||
background: var(--bg-page);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 卡片
|
||||
// ==========================================
|
||||
|
||||
.card {
|
||||
background: var(--bg-content);
|
||||
border-radius: 16rpx;
|
||||
border-radius: 20rpx;
|
||||
margin: 20rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 分割线
|
||||
// ==========================================
|
||||
|
||||
.divider {
|
||||
height: 1rpx;
|
||||
background: var(--divider-color);
|
||||
margin: 0 30rpx;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 头像占位
|
||||
// ==========================================
|
||||
|
||||
.avatar-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -108,7 +403,10 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 空状态
|
||||
// ==========================================
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -118,7 +416,10 @@
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 加载状态
|
||||
// ==========================================
|
||||
|
||||
.loading-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -127,7 +428,10 @@
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 自定义导航栏
|
||||
// ==========================================
|
||||
|
||||
.custom-navbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@@ -135,19 +439,22 @@
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
background: var(--nav-bg);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
padding-top: var(--status-bar-height, 0);
|
||||
border-bottom: 1rpx solid var(--border-subtle);
|
||||
|
||||
.navbar-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 88rpx;
|
||||
padding: 0 30rpx;
|
||||
padding: 0 32rpx;
|
||||
}
|
||||
|
||||
.navbar-title {
|
||||
flex: 1;
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
@@ -158,31 +465,189 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 底部安全区
|
||||
// ==========================================
|
||||
|
||||
.bottom-safe-area {
|
||||
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 浮动按钮
|
||||
// ==========================================
|
||||
|
||||
.fab {
|
||||
position: fixed;
|
||||
right: 30rpx;
|
||||
right: 40rpx;
|
||||
bottom: calc(180rpx + env(safe-area-inset-bottom));
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
width: 112rpx;
|
||||
height: 112rpx;
|
||||
border-radius: 50%;
|
||||
background: var(--color-primary);
|
||||
background: #10b981;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4rpx 20rpx rgba(7, 193, 96, 0.4);
|
||||
box-shadow: 0 10rpx 30rpx rgba(16, 185, 129, 0.3);
|
||||
z-index: 99;
|
||||
transition: transform 0.2s;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 图标按钮
|
||||
// ==========================================
|
||||
|
||||
.icon-btn {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: all 0.15s;
|
||||
|
||||
&:active {
|
||||
background: var(--bg-hover);
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 点击反馈
|
||||
// ==========================================
|
||||
|
||||
.clickable {
|
||||
transition: all 0.15s;
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.pressable {
|
||||
transition: transform 0.15s;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 遮罩层
|
||||
// ==========================================
|
||||
|
||||
.mask-enter {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.mask-active {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 抽屉动画
|
||||
// ==========================================
|
||||
|
||||
.drawer-enter {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.drawer-active {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 菜单动画
|
||||
// ==========================================
|
||||
|
||||
.menu-enter {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(-10rpx);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.menu-active {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 底部弹窗动画
|
||||
// ==========================================
|
||||
|
||||
.bottom-sheet-enter {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
.bottom-sheet-active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 波形可视化 (语音消息)
|
||||
// ==========================================
|
||||
|
||||
.wave-visualizer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4rpx;
|
||||
height: 32rpx;
|
||||
|
||||
.wave-bar {
|
||||
width: 4rpx;
|
||||
height: 8rpx;
|
||||
background: currentColor;
|
||||
border-radius: 4rpx;
|
||||
transition: height 0.1s;
|
||||
|
||||
&.animating {
|
||||
animation: wave 0.8s infinite ease-in-out;
|
||||
|
||||
&:nth-child(1) { animation-delay: 0s; }
|
||||
&:nth-child(2) { animation-delay: 0.1s; }
|
||||
&:nth-child(3) { animation-delay: 0.2s; }
|
||||
&:nth-child(4) { animation-delay: 0.3s; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 头像涟漪动画 (来电)
|
||||
// ==========================================
|
||||
|
||||
.avatar-ripple {
|
||||
position: absolute;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
inset: -20rpx;
|
||||
animation: ripple 2s infinite linear;
|
||||
|
||||
&.delay {
|
||||
animation-delay: 0.5s;
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 脉冲指示器 (通话中)
|
||||
// ==========================================
|
||||
|
||||
.pulse-indicator {
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 50%;
|
||||
background: inherit;
|
||||
animation: pulse-ring 2s infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,107 +1,175 @@
|
||||
/**
|
||||
* 主题变量定义
|
||||
* 浅色/暗色两套主题
|
||||
* 浅色/暗色两套主题 - Warm Stone 暖色调系统
|
||||
*/
|
||||
|
||||
// ==========================================
|
||||
// Warm 色板定义 (用于深色模式)
|
||||
// ==========================================
|
||||
$warm-950: #0c0a09;
|
||||
$warm-900: #1c1917;
|
||||
$warm-800: #292524;
|
||||
$warm-700: #44403c;
|
||||
$warm-600: #57534e;
|
||||
$warm-500: #78716c;
|
||||
$warm-200: #e7e5e4;
|
||||
$warm-100: #f5f5f4;
|
||||
|
||||
// 品牌色
|
||||
$color-brand-light: #4F46E5;
|
||||
$color-brand-dark: #f97316;
|
||||
|
||||
// ==========================================
|
||||
// 浅色主题(默认)
|
||||
// ==========================================
|
||||
page {
|
||||
// 主色调
|
||||
--color-primary: #07c160;
|
||||
--color-primary-dark: #06ad56;
|
||||
--color-danger: #fa5151;
|
||||
--color-warning: #ffc300;
|
||||
--color-success: #07c160;
|
||||
--color-info: #10aeff;
|
||||
// 主色调 - Indigo 品牌色
|
||||
--color-primary: #4F46E5;
|
||||
--color-primary-light: #6366F1;
|
||||
--color-primary-dark: #4338CA;
|
||||
--color-danger: #ef4444;
|
||||
--color-warning: #f97316;
|
||||
--color-success: #10b981;
|
||||
--color-info: #0ea5e9;
|
||||
|
||||
// 背景色
|
||||
--bg-page: #ededed;
|
||||
--bg-page: #f7f8fa;
|
||||
--bg-surface: #ffffff;
|
||||
--bg-content: #ffffff;
|
||||
--bg-hover: #f5f5f5;
|
||||
--bg-highlight: #f2f3f5;
|
||||
--bg-mask: rgba(0, 0, 0, 0.5);
|
||||
--bg-card: #ffffff;
|
||||
|
||||
// 文字颜色
|
||||
--text-primary: #333333;
|
||||
--text-secondary: #666666;
|
||||
--text-tertiary: #999999;
|
||||
--text-placeholder: #cccccc;
|
||||
--text-primary: #1d1d1f;
|
||||
--text-secondary: #6b7280;
|
||||
--text-tertiary: #9ca3af;
|
||||
--text-placeholder: #a1a1a6;
|
||||
--text-link: #576b95;
|
||||
|
||||
// 边框颜色
|
||||
--border-color: #e5e5e5;
|
||||
--divider-color: #f0f0f0;
|
||||
--border-color: rgba(0, 0, 0, 0.05);
|
||||
--border-subtle: rgba(0, 0, 0, 0.05);
|
||||
--divider-color: rgba(0, 0, 0, 0.05);
|
||||
|
||||
// 导航栏
|
||||
--nav-bg: #ededed;
|
||||
--nav-text: #000000;
|
||||
--nav-bg: rgba(255, 255, 255, 0.85);
|
||||
--nav-text: #1d1d1f;
|
||||
|
||||
// 标签栏
|
||||
--tabbar-bg: #f7f7f7;
|
||||
--tabbar-bg: #ffffff;
|
||||
--tabbar-text: #999999;
|
||||
--tabbar-text-active: #07c160;
|
||||
--tabbar-text-active: #4F46E5;
|
||||
|
||||
// 消息气泡
|
||||
--bubble-self: #95ec69;
|
||||
--bubble-self-start: #6366F1;
|
||||
--bubble-self-end: #4F46E5;
|
||||
--bubble-self: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%);
|
||||
--bubble-other: #ffffff;
|
||||
--bubble-system: rgba(0, 0, 0, 0.1);
|
||||
--bubble-system: rgba(0, 0, 0, 0.04);
|
||||
--bubble-shadow: rgba(79, 70, 229, 0.2);
|
||||
|
||||
// 搜索框
|
||||
--search-bg: #f5f5f5;
|
||||
--search-bg: #f1f1f1;
|
||||
|
||||
// 头像
|
||||
--avatar-bg: #07c160;
|
||||
--avatar-bg: #4F46E5;
|
||||
--avatar-text: #ffffff;
|
||||
|
||||
// 未读徽标
|
||||
--badge-bg: #fa5151;
|
||||
--badge-bg: #ef4444;
|
||||
--badge-text: #ffffff;
|
||||
|
||||
// 开关
|
||||
--switch-on: #07c160;
|
||||
--switch-off: #e5e5e5;
|
||||
--switch-on: #4F46E5;
|
||||
--switch-off: #e5e7eb;
|
||||
|
||||
// 状态栏高度变量 (小程序/App会自动注入,H5需fallback)
|
||||
--status-bar-height: 20px;
|
||||
|
||||
// 分段控制器
|
||||
--tab-bg: #e5e7eb;
|
||||
--tab-active-bg: #ffffff;
|
||||
|
||||
// 功能卡片渐变
|
||||
--gradient-orange: linear-gradient(135deg, #f97316, #fb923c);
|
||||
--gradient-blue: linear-gradient(135deg, #0ea5e9, #38bdf8);
|
||||
--gradient-indigo: linear-gradient(135deg, #6366F1, #4F46E5);
|
||||
--gradient-green: linear-gradient(135deg, #10b981, #34d399);
|
||||
}
|
||||
|
||||
// 暗色主题 (Dark Mode - 高级灰优化)
|
||||
// ==========================================
|
||||
// 暗色主题 (Dark Mode - Warm Stone 暖色调)
|
||||
// ==========================================
|
||||
page.dark,
|
||||
.dark,
|
||||
html.dark,
|
||||
body.dark {
|
||||
// 背景色 - 使用 #1c1c1e 替代纯黑
|
||||
--bg-page: #1c1c1e;
|
||||
--bg-content: #2c2c2e;
|
||||
--bg-hover: #3a3a3c;
|
||||
// 主色调 - Orange 品牌色
|
||||
--color-primary: #f97316;
|
||||
--color-primary-light: #fb923c;
|
||||
--color-primary-dark: #ea580c;
|
||||
--color-danger: #ef4444;
|
||||
--color-warning: #fbbf24;
|
||||
--color-success: #10b981;
|
||||
--color-info: #38bdf8;
|
||||
|
||||
// 背景色 - Warm Stone 暖色调
|
||||
--bg-page: #{$warm-900};
|
||||
--bg-surface: #{$warm-800};
|
||||
--bg-content: #{$warm-800};
|
||||
--bg-hover: #{$warm-700};
|
||||
--bg-highlight: #{$warm-700};
|
||||
--bg-mask: rgba(0, 0, 0, 0.7);
|
||||
--bg-card: #2c2c2e;
|
||||
--bg-card: #{$warm-800};
|
||||
|
||||
// 文字颜色
|
||||
--text-primary: #f2f2f7; // 柔和白
|
||||
--text-secondary: #aeaeb2;
|
||||
--text-tertiary: #636366;
|
||||
--text-placeholder: #505055;
|
||||
--text-link: #7b9ad0;
|
||||
--text-primary: #{$warm-100};
|
||||
--text-secondary: #{$warm-200};
|
||||
--text-tertiary: #{$warm-500};
|
||||
--text-placeholder: #{$warm-600};
|
||||
--text-link: #7e95c5;
|
||||
|
||||
// 边框颜色
|
||||
--border-color: #38383a;
|
||||
--divider-color: #38383a;
|
||||
--border-color: #{$warm-700};
|
||||
--border-subtle: rgba(255, 255, 255, 0.1);
|
||||
--divider-color: #{$warm-700};
|
||||
|
||||
// 导航栏
|
||||
--nav-bg: #1c1c1e;
|
||||
--nav-text: #ffffff;
|
||||
--nav-bg: rgba(28, 25, 23, 0.85);
|
||||
--nav-text: #{$warm-100};
|
||||
|
||||
// 标签栏
|
||||
--tabbar-bg: #1c1c1e;
|
||||
--tabbar-bg: #{$warm-900};
|
||||
--tabbar-text: #{$warm-500};
|
||||
--tabbar-text-active: #f97316;
|
||||
|
||||
// 消息气泡
|
||||
--bubble-self: #2d5c2e; // 深绿
|
||||
--bubble-other: #2c2c2e;
|
||||
--bubble-system: rgba(255, 255, 255, 0.1);
|
||||
// 消息气泡 - 深色模式保持渐变
|
||||
--bubble-self-start: #6366F1;
|
||||
--bubble-self-end: #4F46E5;
|
||||
--bubble-self: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%);
|
||||
--bubble-other: #262626;
|
||||
--bubble-system: rgba(255, 255, 255, 0.08);
|
||||
--bubble-shadow: none;
|
||||
|
||||
// 搜索框
|
||||
--search-bg: #2c2c2e;
|
||||
--search-bg: #{$warm-800};
|
||||
|
||||
// 头像
|
||||
--avatar-bg: #f97316;
|
||||
|
||||
// 开关
|
||||
--switch-off: #3a3a3c;
|
||||
--switch-on: #f97316;
|
||||
--switch-off: #{$warm-700};
|
||||
|
||||
// 分段控制器
|
||||
--tab-bg: #{$warm-800};
|
||||
--tab-active-bg: #{$warm-700};
|
||||
|
||||
// 功能卡片渐变 (暖色调版本)
|
||||
--gradient-orange: linear-gradient(135deg, #ea580c, #f97316);
|
||||
--gradient-blue: linear-gradient(135deg, #0284c7, #0ea5e9);
|
||||
--gradient-indigo: linear-gradient(135deg, #c2410c, #ea580c);
|
||||
--gradient-green: linear-gradient(135deg, #059669, #10b981);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user