UI设计
This commit is contained in:
25
src/components/call/GlobalCallProvider.vue
Normal file
25
src/components/call/GlobalCallProvider.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<!-- 全局群通话来电覆盖层 -->
|
||||
<group-call-incoming />
|
||||
|
||||
<!-- 全局群通话窗口 -->
|
||||
<group-call-window />
|
||||
|
||||
<!-- 群聊通话横幅 (有正在进行的通话但用户未加入) -->
|
||||
<group-call-banner />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { useGroupWebRTC } from '@/composables/useGroupWebRTC'
|
||||
import GroupCallIncoming from './GroupCallIncoming.vue'
|
||||
import GroupCallWindow from './GroupCallWindow.vue'
|
||||
import GroupCallBanner from './GroupCallBanner.vue'
|
||||
|
||||
const { initListener } = useGroupWebRTC()
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化信令监听器
|
||||
initListener()
|
||||
})
|
||||
</script>
|
||||
298
src/components/common/AppDrawer.vue
Normal file
298
src/components/common/AppDrawer.vue
Normal file
@@ -0,0 +1,298 @@
|
||||
<template>
|
||||
<view class="drawer-container" :class="{ dark: isDark }">
|
||||
<!-- 遮罩层 -->
|
||||
<view
|
||||
class="drawer-mask smooth-transition"
|
||||
:class="{ 'mask-active': modelValue, 'mask-enter': !modelValue }"
|
||||
@click="close"
|
||||
/>
|
||||
|
||||
<!-- 抽屉内容 -->
|
||||
<view
|
||||
class="drawer-content smooth-transition"
|
||||
:class="{ 'drawer-active': modelValue, 'drawer-enter': !modelValue }"
|
||||
>
|
||||
<!-- 头部用户信息 -->
|
||||
<view class="drawer-header">
|
||||
<view class="avatar-wrap">
|
||||
<image
|
||||
v-if="user?.avatar"
|
||||
class="avatar"
|
||||
:src="resolveImageUrl(user.avatar)"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view v-else class="avatar avatar-placeholder">
|
||||
{{ (user?.name || '?').charAt(0) }}
|
||||
</view>
|
||||
</view>
|
||||
<text class="username">{{ user?.name || '请登录' }}</text>
|
||||
<text class="bio">{{ user?.desc || '编辑个签,展示你的独特个性' }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 菜单列表 -->
|
||||
<view class="drawer-menu">
|
||||
<view class="menu-item" @click="goProfile">
|
||||
<view class="menu-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<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>
|
||||
</view>
|
||||
<text class="menu-text">个人资料</text>
|
||||
</view>
|
||||
<view class="menu-item" @click="goSettings">
|
||||
<view class="menu-icon">
|
||||
<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>
|
||||
<text class="menu-text">设置</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部退出按钮 -->
|
||||
<view class="drawer-footer">
|
||||
<view class="logout-btn" @click="handleLogout">
|
||||
退出登录
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useAuthStore } from '@/stores'
|
||||
import { useTheme } from '@/composables/useTheme'
|
||||
import { resolveImageUrl } from '@/utils/image'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'logout'): void
|
||||
}>()
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const { isDark } = useTheme()
|
||||
|
||||
const user = computed(() => authStore.user)
|
||||
|
||||
function close() {
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
|
||||
function goProfile() {
|
||||
close()
|
||||
uni.navigateTo({ url: '/pages/profile/index' })
|
||||
}
|
||||
|
||||
function goSettings() {
|
||||
close()
|
||||
uni.navigateTo({ url: '/pages/settings/index' })
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
emit('logout')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// ==========================================
|
||||
// 抽屉容器
|
||||
// ==========================================
|
||||
.drawer-container {
|
||||
--drawer-bg: #ffffff;
|
||||
--header-bg: #ffffff;
|
||||
--menu-hover: #f5f5f5;
|
||||
--menu-text: #374151;
|
||||
--menu-icon: #6b7280;
|
||||
--logout-bg: rgba(239, 68, 68, 0.08);
|
||||
--border-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.drawer-container.dark {
|
||||
--drawer-bg: #1c1917;
|
||||
--header-bg: #1c1917;
|
||||
--menu-hover: #292524;
|
||||
--menu-text: #e7e5e4;
|
||||
--menu-icon: #78716c;
|
||||
--logout-bg: #292524;
|
||||
--border-color: #44403c;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 遮罩层
|
||||
// ==========================================
|
||||
.drawer-mask {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(2px);
|
||||
-webkit-backdrop-filter: blur(2px);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.mask-enter {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.mask-active {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 抽屉内容
|
||||
// ==========================================
|
||||
.drawer-content {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 75%;
|
||||
height: 100%;
|
||||
background: var(--drawer-bg);
|
||||
z-index: 1001;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 16rpx 0 48rpx rgba(0, 0, 0, 0.1);
|
||||
border-right: 1rpx solid var(--border-color);
|
||||
}
|
||||
|
||||
.drawer-enter {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.drawer-active {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.smooth-transition {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 头部区域
|
||||
// ==========================================
|
||||
.drawer-header {
|
||||
padding: 160rpx 64rpx 64rpx;
|
||||
border-bottom: 1rpx solid var(--border-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.avatar-wrap {
|
||||
width: 128rpx;
|
||||
height: 128rpx;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
margin-bottom: 32rpx;
|
||||
border: 2rpx solid var(--border-color);
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.avatar-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #4F46E5;
|
||||
color: #ffffff;
|
||||
font-size: 48rpx;
|
||||
font-weight: 600;
|
||||
|
||||
.dark & {
|
||||
background: #f97316;
|
||||
}
|
||||
}
|
||||
|
||||
.username {
|
||||
font-size: 42rpx;
|
||||
font-weight: 700;
|
||||
color: var(--menu-text);
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.bio {
|
||||
font-size: 28rpx;
|
||||
color: var(--menu-icon);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 菜单列表
|
||||
// ==========================================
|
||||
.drawer-menu {
|
||||
flex: 1;
|
||||
padding: 16rpx;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
padding: 24rpx;
|
||||
border-radius: 16rpx;
|
||||
transition: background 0.2s;
|
||||
cursor: pointer;
|
||||
|
||||
&:active {
|
||||
background: var(--menu-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
color: var(--menu-icon);
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: var(--menu-text);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 底部退出按钮
|
||||
// ==========================================
|
||||
.drawer-footer {
|
||||
padding: 48rpx;
|
||||
padding-bottom: calc(48rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--logout-bg);
|
||||
color: #ef4444;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
border-radius: 16rpx;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:active {
|
||||
opacity: 0.8;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
175
src/components/common/PlusMenu.vue
Normal file
175
src/components/common/PlusMenu.vue
Normal file
@@ -0,0 +1,175 @@
|
||||
<template>
|
||||
<view class="plus-menu-container" :class="{ dark: isDark }">
|
||||
<!-- 遮罩层 -->
|
||||
<view
|
||||
v-if="modelValue"
|
||||
class="menu-backdrop"
|
||||
@click="close"
|
||||
/>
|
||||
|
||||
<!-- 菜单弹窗 (与设计稿一致) -->
|
||||
<view
|
||||
class="plus-menu"
|
||||
:class="{ 'menu-active': modelValue, 'menu-enter': !modelValue }"
|
||||
>
|
||||
<!-- 创建群聊 -->
|
||||
<view class="menu-item" @click="handleClick('createGroup')">
|
||||
<svg class="menu-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<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"/>
|
||||
<line x1="12" y1="7" x2="12" y2="13"/>
|
||||
</svg>
|
||||
<text class="menu-text">创建群聊</text>
|
||||
</view>
|
||||
|
||||
<!-- 添加好友 -->
|
||||
<view class="menu-item" @click="handleClick('addFriend')">
|
||||
<svg class="menu-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>
|
||||
<text class="menu-text">添加好友</text>
|
||||
</view>
|
||||
|
||||
<!-- 扫一扫 -->
|
||||
<view class="menu-item" @click="handleClick('scan')">
|
||||
<svg class="menu-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<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>
|
||||
<text class="menu-text">扫一扫</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTheme } from '@/composables/useTheme'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'select', key: string): void
|
||||
}>()
|
||||
|
||||
const { isDark } = useTheme()
|
||||
|
||||
function close() {
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
|
||||
function handleClick(key: string) {
|
||||
emit('select', key)
|
||||
close()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// ==========================================
|
||||
// 浅色模式变量
|
||||
// ==========================================
|
||||
.plus-menu-container {
|
||||
--menu-bg: #ffffff;
|
||||
--menu-text: #374151;
|
||||
--menu-icon: #4b5563;
|
||||
--menu-hover: #f5f5f5;
|
||||
--menu-border: rgba(0, 0, 0, 0.05);
|
||||
--menu-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 深色模式变量
|
||||
// ==========================================
|
||||
.plus-menu-container.dark {
|
||||
--menu-bg: #292524;
|
||||
--menu-text: #e7e5e4;
|
||||
--menu-icon: #a8a29e;
|
||||
--menu-hover: #44403c;
|
||||
--menu-border: rgba(255, 255, 255, 0.1);
|
||||
--menu-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 遮罩层
|
||||
// ==========================================
|
||||
.menu-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 998;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 菜单弹窗 (与设计稿一致)
|
||||
// ==========================================
|
||||
.plus-menu {
|
||||
position: fixed;
|
||||
top: calc(var(--status-bar-height, 20px) + 180rpx);
|
||||
right: 32rpx;
|
||||
width: 288rpx;
|
||||
background: var(--menu-bg);
|
||||
border-radius: 24rpx;
|
||||
padding: 8rpx 0;
|
||||
z-index: 999;
|
||||
box-shadow: var(--menu-shadow);
|
||||
border: 2rpx solid var(--menu-border);
|
||||
transform-origin: top right;
|
||||
transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.menu-enter {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(-20rpx);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.menu-active {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 菜单项 (与设计稿一致)
|
||||
// ==========================================
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
padding: 28rpx 28rpx;
|
||||
transition: background 0.15s;
|
||||
border-bottom: 1rpx solid var(--menu-border);
|
||||
cursor: pointer;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: var(--menu-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
color: var(--menu-icon);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.menu-text {
|
||||
font-size: 24rpx;
|
||||
color: var(--menu-text);
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user