UI优化
This commit is contained in:
21
src/App.vue
21
src/App.vue
@@ -13,7 +13,7 @@ const conversationStore = useConversationStore()
|
|||||||
function handleGlobalMessage(message: ChatMessage) {
|
function handleGlobalMessage(message: ChatMessage) {
|
||||||
// 忽略信令消息
|
// 忽略信令消息
|
||||||
if (message.message_type === 6) return
|
if (message.message_type === 6) return
|
||||||
|
|
||||||
// 更新会话列表
|
// 更新会话列表
|
||||||
const isSelf = message.sender_user_id === authStore.user?.id
|
const isSelf = message.sender_user_id === authStore.user?.id
|
||||||
conversationStore.handleMessageUpdate(message, isSelf, false)
|
conversationStore.handleMessageUpdate(message, isSelf, false)
|
||||||
@@ -23,7 +23,7 @@ function handleGlobalMessage(message: ChatMessage) {
|
|||||||
async function initWebSocket() {
|
async function initWebSocket() {
|
||||||
const userId = authStore.user?.id
|
const userId = authStore.user?.id
|
||||||
if (!userId) return
|
if (!userId) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await wsManager.connect(userId)
|
await wsManager.connect(userId)
|
||||||
// 注册全局消息处理器
|
// 注册全局消息处理器
|
||||||
@@ -44,7 +44,7 @@ onShow(async () => {
|
|||||||
console.log('App Show')
|
console.log('App Show')
|
||||||
// 检查登录状态
|
// 检查登录状态
|
||||||
const isAuth = await authStore.checkAuth()
|
const isAuth = await authStore.checkAuth()
|
||||||
|
|
||||||
// 如果已登录,初始化 WebSocket
|
// 如果已登录,初始化 WebSocket
|
||||||
if (isAuth && authStore.user?.id) {
|
if (isAuth && authStore.user?.id) {
|
||||||
initWebSocket()
|
initWebSocket()
|
||||||
@@ -56,6 +56,11 @@ onShow(async () => {
|
|||||||
@import '@/styles/variables.scss';
|
@import '@/styles/variables.scss';
|
||||||
@import '@/styles/common.scss';
|
@import '@/styles/common.scss';
|
||||||
|
|
||||||
|
/* 移除全局硬编码 padding,交由各页面 nav-bar 处理 */
|
||||||
|
#app {
|
||||||
|
/* padding-top: calc(var(--status-bar-height) + 40rpx); REMOVED */
|
||||||
|
}
|
||||||
|
|
||||||
/* 全局样式 */
|
/* 全局样式 */
|
||||||
page {
|
page {
|
||||||
background-color: var(--bg-page);
|
background-color: var(--bg-page);
|
||||||
@@ -64,11 +69,11 @@ page {
|
|||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
|
||||||
// wot-design-uni 主题色覆盖
|
// wot-design-uni 主题色覆盖 (使用 CSS 变量)
|
||||||
--wot-color-theme: #07c160;
|
--wot-color-theme: var(--color-primary);
|
||||||
--wot-color-success: #07c160;
|
--wot-color-success: var(--color-success);
|
||||||
--wot-color-danger: #fa5151;
|
--wot-color-danger: var(--color-danger);
|
||||||
--wot-color-warning: #ffc300;
|
--wot-color-warning: var(--color-warning);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 暗色模式下的 wot-design-uni 组件适配 */
|
/* 暗色模式下的 wot-design-uni 组件适配 */
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,223 +1,344 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="add-page" :class="{ dark: isDark }">
|
<wd-config-provider :theme="isDark ? 'dark' : 'light'">
|
||||||
<!-- 导航栏 -->
|
<view class="page-container" :class="{ 'theme-dark': isDark }">
|
||||||
<app-nav-bar title="添加好友" />
|
<!-- 顶部背景装饰 -->
|
||||||
|
<view class="page-bg-decoration" />
|
||||||
<!-- 搜索区域 -->
|
|
||||||
<view class="search-section">
|
|
||||||
<wd-search
|
|
||||||
v-model="keyword"
|
|
||||||
placeholder="输入用户ID或手机号搜索"
|
|
||||||
show-action
|
|
||||||
@search="doSearch"
|
|
||||||
@cancel="keyword = ''"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 搜索结果 -->
|
<!-- 导航栏 -->
|
||||||
<view v-if="searched" class="results-section">
|
<app-nav-bar title="添加好友" custom-style="background: transparent;" />
|
||||||
<view v-if="loading" class="loading-state">
|
|
||||||
<wd-loading />
|
|
||||||
<text>搜索中...</text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<wd-status-tip v-else-if="results.length === 0" tip="未找到相关用户" />
|
<view class="content-body">
|
||||||
|
<!-- 头部标题区 -->
|
||||||
|
<view class="header-area">
|
||||||
|
<text class="main-title">发现新朋友</text>
|
||||||
|
<text class="sub-title">Expanding your circle</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
<template v-else>
|
<!-- 搜索区域 -->
|
||||||
<view
|
<view class="search-section">
|
||||||
v-for="item in results"
|
<wd-search
|
||||||
:key="item.id"
|
v-model="keyword"
|
||||||
class="result-item"
|
placeholder="搜索账号 / 手机号"
|
||||||
>
|
hide-cancel
|
||||||
<app-avatar
|
placeholder-left
|
||||||
:src="item.avatar"
|
custom-class="glass-search"
|
||||||
:name="item.name"
|
@search="doSearch"
|
||||||
:size="96"
|
@clear="keyword = ''"
|
||||||
radius="8rpx"
|
>
|
||||||
/>
|
<template #suffix>
|
||||||
|
<view class="search-btn" @click="doSearch">
|
||||||
|
<wd-icon name="search" size="36rpx" />
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
</wd-search>
|
||||||
|
</view>
|
||||||
|
|
||||||
<view class="user-info">
|
<!-- 搜索结果 -->
|
||||||
<text class="name">{{ item.name }}</text>
|
<view v-if="searched" class="result-section">
|
||||||
<text class="id">ID: {{ item.id }}</text>
|
<view class="section-header">
|
||||||
|
<text class="section-title">搜索结果</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<wd-button type="primary" size="small" @click="addFriend(item)">添加</wd-button>
|
<!-- 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
|
||||||
|
v-for="item in results"
|
||||||
|
:key="item.id"
|
||||||
|
class="user-card"
|
||||||
|
>
|
||||||
|
<app-avatar
|
||||||
|
:src="item.avatar"
|
||||||
|
:name="item.name"
|
||||||
|
:size="110"
|
||||||
|
radius="36rpx"
|
||||||
|
/>
|
||||||
|
<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>
|
||||||
|
</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>
|
</view>
|
||||||
</template>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 扫码添加 -->
|
|
||||||
<view class="scan-section" @click="scanQrCode">
|
|
||||||
<view class="scan-icon">
|
|
||||||
<wd-icon name="scan" size="64rpx" color="var(--color-primary)" />
|
|
||||||
</view>
|
</view>
|
||||||
<text class="scan-text">扫一扫添加好友</text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<wd-toast />
|
<wd-toast />
|
||||||
<wd-message-box />
|
<wd-message-box />
|
||||||
</view>
|
</view>
|
||||||
|
</wd-config-provider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import * as contactApi from '@/api/modules/contact'
|
import * as contactApi from '@/api/modules/contact'
|
||||||
import { generateColor } from '@/utils/format'
|
|
||||||
import { useToast, useMessage } from 'wot-design-uni'
|
import { useToast, useMessage } from 'wot-design-uni'
|
||||||
import { useTheme } from '@/composables/useTheme'
|
import { useTheme } from '@/composables/useTheme'
|
||||||
import AppAvatar from '@/components/common/AppAvatar.vue'
|
import AppAvatar from '@/components/common/AppAvatar.vue'
|
||||||
import type { User } from '@/types/api'
|
import type { User } from '@/types/api'
|
||||||
|
|
||||||
const toast = useToast()
|
// --- 保持逻辑完全不变 ---
|
||||||
const message = useMessage()
|
const toast = useToast(); const message = useMessage(); const { isDark } = useTheme();
|
||||||
const { isDark } = useTheme()
|
const keyword = ref(''); const loading = ref(false); const searched = ref(false); const results = ref<User[]>([]);
|
||||||
|
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 } }
|
||||||
const keyword = ref('')
|
async function addFriend(user: User) { try { const { value } = await message.prompt({ title: '添加好友', inputPlaceholder: '请输入验证消息', inputValue: '你好,我是...' }); await contactApi.addFriend({ to_user_id: user.id, message: value || '' }); toast.success('申请已发送') } catch (e: any) { if (e !== 'cancel') { toast.error('发送失败') } } }
|
||||||
const loading = ref(false)
|
function scanQrCode() { uni.scanCode({ onlyFromCamera: false, success: (res) => { console.log('扫码结果:', res.result); toast.show('扫码功能开发中') }, fail: () => { toast.error('扫码失败') } }) }
|
||||||
const searched = ref(false)
|
|
||||||
const results = ref<User[]>([])
|
|
||||||
|
|
||||||
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: '添加好友',
|
|
||||||
inputPlaceholder: '请输入验证消息',
|
|
||||||
inputValue: '你好,我是...'
|
|
||||||
})
|
|
||||||
|
|
||||||
await contactApi.addFriend({
|
|
||||||
to_user_id: user.id,
|
|
||||||
message: value || ''
|
|
||||||
})
|
|
||||||
toast.success('申请已发送')
|
|
||||||
} catch (e: any) {
|
|
||||||
if (e !== 'cancel') {
|
|
||||||
toast.error('发送失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function scanQrCode() {
|
|
||||||
uni.scanCode({
|
|
||||||
onlyFromCamera: false,
|
|
||||||
success: (res) => {
|
|
||||||
console.log('扫码结果:', res.result)
|
|
||||||
// TODO: 处理扫码结果
|
|
||||||
toast.show('扫码功能开发中')
|
|
||||||
},
|
|
||||||
fail: () => {
|
|
||||||
toast.error('扫码失败')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.add-page {
|
.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);
|
||||||
|
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: var(--bg-page);
|
background: var(--bg-base);
|
||||||
}
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
.search-section {
|
&.theme-dark {
|
||||||
background: var(--bg-content);
|
--bg-base: #121212; /* 高级灰 */
|
||||||
padding: 10rpx 0;
|
--bg-card: #1C1C1E; /* 卡片灰 */
|
||||||
}
|
--text-main: #F2F3F5;
|
||||||
|
--text-sub: #86909C;
|
||||||
.results-section {
|
--shadow-sm: 0 4rpx 16rpx rgba(0, 0, 0, 0.2);
|
||||||
background: var(--bg-content);
|
|
||||||
margin-top: 20rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-state {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 60rpx;
|
|
||||||
color: var(--text-tertiary);
|
|
||||||
gap: 20rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 24rpx 30rpx;
|
|
||||||
border-bottom: 1rpx solid var(--divider-color);
|
|
||||||
|
|
||||||
.avatar-placeholder {
|
|
||||||
width: 96rpx;
|
|
||||||
height: 96rpx;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: 8rpx;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 36rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-info {
|
/* 背景装饰球 */
|
||||||
flex: 1;
|
.page-bg-decoration {
|
||||||
margin-left: 24rpx;
|
position: absolute;
|
||||||
min-width: 0;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
.name {
|
.content-body {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 20rpx 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 头部 */
|
||||||
|
.header-area {
|
||||||
|
margin: 20rpx 0 40rpx;
|
||||||
|
.main-title {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 32rpx;
|
font-size: 48rpx;
|
||||||
color: var(--text-primary);
|
font-weight: 800;
|
||||||
|
color: var(--text-main);
|
||||||
|
letter-spacing: -1rpx;
|
||||||
|
}
|
||||||
|
.sub-title {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: var(--text-sub);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 2rpx;
|
||||||
.id {
|
|
||||||
display: block;
|
|
||||||
font-size: 26rpx;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
margin-top: 8rpx;
|
margin-top: 8rpx;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.scan-section {
|
/* 搜索栏定制 */
|
||||||
|
.search-section {
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
: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%;
|
||||||
|
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); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 结果列表 */
|
||||||
|
.section-header {
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
.section-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-main);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-box, .empty-box {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
padding: 100rpx 0;
|
||||||
padding: 80rpx;
|
gap: 24rpx;
|
||||||
background: var(--bg-content);
|
color: var(--text-sub);
|
||||||
margin-top: 20rpx;
|
font-size: 26rpx;
|
||||||
|
}
|
||||||
|
.empty-icon { font-size: 60rpx; opacity: 0.5; }
|
||||||
|
|
||||||
.scan-icon {
|
.user-card {
|
||||||
width: 120rpx;
|
background: var(--bg-card);
|
||||||
height: 120rpx;
|
padding: 24rpx;
|
||||||
|
border-radius: 32rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
animation: slideUp 0.3s ease-out;
|
||||||
|
|
||||||
|
.card-info {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 24rpx;
|
||||||
|
.user-name {
|
||||||
|
display: block;
|
||||||
|
font-size: 34rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-main);
|
||||||
|
}
|
||||||
|
.user-id {
|
||||||
|
display: block;
|
||||||
|
margin-top: 6rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: var(--text-sub);
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-wrap {
|
||||||
|
width: 80rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: var(--text-main); /* 黑色/白色按钮 */
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: var(--bg-hover);
|
transition: all 0.2s;
|
||||||
border-radius: 50%;
|
|
||||||
margin-bottom: 24rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scan-text {
|
.theme-dark & { background: #333; }
|
||||||
font-size: 30rpx;
|
|
||||||
color: var(--text-secondary);
|
&:active { transform: scale(0.92); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 功能卡片 (扫一扫) */
|
||||||
|
.feature-cards {
|
||||||
|
margin-top: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card {
|
||||||
|
position: relative;
|
||||||
|
border-radius: 40rpx;
|
||||||
|
padding: 40rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
|
||||||
|
&.scan-card {
|
||||||
|
background: var(--primary-gradient);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片内部纹理 */
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -50%;
|
||||||
|
left: -20%;
|
||||||
|
width: 200%;
|
||||||
|
height: 200%;
|
||||||
|
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 60%);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 24rpx;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from { opacity: 0; transform: translateY(20rpx); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,73 +1,100 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="detail-page" :class="{ dark: isDark }">
|
<view class="detail-container" :class="{ 'theme-dark': isDark }">
|
||||||
<!-- 导航栏 -->
|
<!-- 头部背景区 (视差效果) -->
|
||||||
<app-nav-bar title="好友详情" />
|
<view class="profile-header">
|
||||||
|
<view class="blur-bg" :style="headerBgStyle" />
|
||||||
<view v-if="loading" class="loading-state">
|
<view class="gradient-mask" />
|
||||||
<wd-loading />
|
<app-nav-bar custom-style="background: transparent;" title-style="color: transparent;" />
|
||||||
<text>加载中...</text>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<template v-else-if="contact">
|
<!-- 主体内容 (浮动卡片) -->
|
||||||
<!-- 用户信息 -->
|
<scroll-view scroll-y class="main-content" v-if="!loading && contact">
|
||||||
<view class="header-section">
|
<view class="content-wrapper">
|
||||||
<app-avatar
|
|
||||||
:src="contact.user?.avatar"
|
<!-- 用户身份卡片 -->
|
||||||
:name="contact.remark_name || contact.user?.name"
|
<view class="identity-card">
|
||||||
:size="140"
|
<view class="avatar-wrapper">
|
||||||
radius="12rpx"
|
<app-avatar
|
||||||
/>
|
:src="contact.user?.avatar"
|
||||||
<view class="user-info">
|
:name="contact.remark_name || contact.user?.name"
|
||||||
<text class="name">{{ contact.remark_name || contact.user?.name || '未知' }}</text>
|
:size="180"
|
||||||
<text class="id">ID: {{ contact.user?.id || '-' }}</text>
|
radius="50%"
|
||||||
|
custom-style="border: 6rpx 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>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- 设置组:交互 -->
|
||||||
|
<view class="settings-group">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<view class="spacer" />
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<!-- Loading -->
|
||||||
|
<view v-else-if="loading" class="center-loading">
|
||||||
|
<wd-loading size="60rpx" color="#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" />
|
||||||
|
</view>
|
||||||
|
<view class="dock-btn primary" @click="goChat">
|
||||||
|
<wd-icon name="chat-filled" size="40rpx" />
|
||||||
|
<text>发消息</text>
|
||||||
|
</view>
|
||||||
|
<view class="dock-btn secondary" @click="startVideoCall">
|
||||||
|
<wd-icon name="video" size="44rpx" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 基本信息 -->
|
<view class="danger-btn" @click="deleteFriend">
|
||||||
<wd-cell-group>
|
<text>删除好友</text>
|
||||||
<wd-cell title="备注名" :value="contact.remark_name || '未设置'" is-link @click="editRemark" />
|
|
||||||
<wd-cell title="个性签名" :value="contact.user?.desc || '暂无'" />
|
|
||||||
<wd-cell title="地区" :value="contact.user?.region || '未知'" />
|
|
||||||
</wd-cell-group>
|
|
||||||
|
|
||||||
<!-- 设置 -->
|
|
||||||
<wd-cell-group title="聊天设置">
|
|
||||||
<wd-cell title="置顶聊天" center>
|
|
||||||
<template #value>
|
|
||||||
<wd-switch v-model="contact.is_top" @change="toggleTop" />
|
|
||||||
</template>
|
|
||||||
</wd-cell>
|
|
||||||
<wd-cell title="消息免打扰" center>
|
|
||||||
<template #value>
|
|
||||||
<wd-switch v-model="contact.is_muted" @change="toggleMute" />
|
|
||||||
</template>
|
|
||||||
</wd-cell>
|
|
||||||
<wd-cell title="加入黑名单" center>
|
|
||||||
<template #value>
|
|
||||||
<wd-switch v-model="contact.is_blocked" @change="toggleBlock" />
|
|
||||||
</template>
|
|
||||||
</wd-cell>
|
|
||||||
</wd-cell-group>
|
|
||||||
|
|
||||||
<!-- 操作按钮 -->
|
|
||||||
<view class="actions-section">
|
|
||||||
<view class="action-row">
|
|
||||||
<wd-button type="primary" class="flex-btn" @click="goChat">
|
|
||||||
<wd-icon name="chat" size="36rpx" />
|
|
||||||
<text>发消息</text>
|
|
||||||
</wd-button>
|
|
||||||
<wd-button type="info" class="flex-btn" @click="startAudioCall">
|
|
||||||
<wd-icon name="phone" size="36rpx" />
|
|
||||||
<text>语音</text>
|
|
||||||
</wd-button>
|
|
||||||
<wd-button type="success" class="flex-btn" @click="startVideoCall">
|
|
||||||
<wd-icon name="video" size="36rpx" />
|
|
||||||
<text>视频</text>
|
|
||||||
</wd-button>
|
|
||||||
</view>
|
|
||||||
<wd-button type="error" block plain @click="deleteFriend">删除好友</wd-button>
|
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</view>
|
||||||
|
|
||||||
<wd-toast />
|
<wd-toast />
|
||||||
<wd-message-box />
|
<wd-message-box />
|
||||||
@@ -75,236 +102,261 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
import { onLoad } from '@dcloudio/uni-app'
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
import * as contactApi from '@/api/modules/contact'
|
import * as contactApi from '@/api/modules/contact'
|
||||||
import { generateColor } from '@/utils/format'
|
|
||||||
import { resolveImageUrl } from '@/utils/image'
|
|
||||||
import { useToast, useMessage } from 'wot-design-uni'
|
import { useToast, useMessage } from 'wot-design-uni'
|
||||||
import { useTheme } from '@/composables/useTheme'
|
import { useTheme } from '@/composables/useTheme'
|
||||||
import AppAvatar from '@/components/common/AppAvatar.vue'
|
import AppAvatar from '@/components/common/AppAvatar.vue'
|
||||||
import type { Contact } from '@/types/api'
|
import type { Contact } from '@/types/api'
|
||||||
|
|
||||||
const toast = useToast()
|
// --- 逻辑保持不变 ---
|
||||||
const messageBox = useMessage()
|
const toast = useToast(); const messageBox = useMessage(); const { isDark } = useTheme();
|
||||||
const { isDark } = useTheme()
|
const loading = ref(true); const contact = ref<Contact | null>(null); const contactId = ref(''); const userId = ref('');
|
||||||
|
|
||||||
const loading = ref(true)
|
const headerBgStyle = computed(() => {
|
||||||
const contact = ref<Contact | null>(null)
|
const url = contact.value?.user?.avatar || '';
|
||||||
const contactId = ref('')
|
return url ? `background-image: url(${url});` : 'background: var(--bg-card);';
|
||||||
const userId = ref('')
|
|
||||||
|
|
||||||
onLoad((options: any) => {
|
|
||||||
contactId.value = options?.id || ''
|
|
||||||
userId.value = options?.userId || ''
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onLoad((options: any) => { contactId.value = options?.id || ''; userId.value = options?.userId || '' })
|
||||||
if (contactId.value) {
|
onMounted(async () => { if (contactId.value) { await loadContact() } else if (userId.value) { await loadContactByUserId() } })
|
||||||
await loadContact()
|
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 } }
|
||||||
} else if (userId.value) {
|
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 } }
|
||||||
await loadContactByUserId()
|
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` }) }
|
||||||
|
async function editRemark() { if (!contact.value) return; try { const { value } = await messageBox.prompt({ title: '修改备注名', inputValue: contact.value.remark_name || '', inputPlaceholder: '请输入备注名' }); await contactApi.updateContact(contactId.value, { remark_name: value }); contact.value.remark_name = value; toast.success('修改成功') } catch (e: any) { if (e !== 'cancel') { toast.error('修改失败') } } }
|
||||||
async function loadContact() {
|
async function toggleTop() { if (!contact.value) return; try { await contactApi.updateContact(contactId.value, { is_top: contact.value.is_top }); toast.success(contact.value.is_top ? '已置顶' : '已取消置顶') } catch { contact.value.is_top = !contact.value.is_top; toast.error('操作失败') } }
|
||||||
loading.value = true
|
async function toggleMute() { if (!contact.value) return; try { await contactApi.updateContact(contactId.value, { is_muted: contact.value.is_muted }); toast.success(contact.value.is_muted ? '已开启免打扰' : '已关闭免打扰') } catch { contact.value.is_muted = !contact.value.is_muted; toast.error('操作失败') } }
|
||||||
try {
|
async function toggleBlock() { if (!contact.value) return; try { await contactApi.updateContact(contactId.value, { is_blocked: contact.value.is_blocked }); toast.success(contact.value.is_blocked ? '已加入黑名单' : '已移出黑名单') } catch { contact.value.is_blocked = !contact.value.is_blocked; toast.error('操作失败') } }
|
||||||
contact.value = await contactApi.getContactDetail(contactId.value)
|
async function deleteFriend() { try { await messageBox.confirm({ title: '提示', msg: '确定要删除该好友吗?删除后将无法恢复' }); await contactApi.deleteContact(contactId.value); toast.success('删除成功'); setTimeout(() => { uni.navigateBack() }, 1000) } catch (e: any) { if (e !== 'cancel') { toast.error('删除失败') } } }
|
||||||
} catch (error) {
|
|
||||||
console.error('加载联系人失败:', error)
|
|
||||||
toast.error('加载失败')
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 通过用户 ID 查找联系人
|
|
||||||
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 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`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function editRemark() {
|
|
||||||
if (!contact.value) return
|
|
||||||
try {
|
|
||||||
const { value } = await messageBox.prompt({
|
|
||||||
title: '修改备注名',
|
|
||||||
inputValue: contact.value.remark_name || '',
|
|
||||||
inputPlaceholder: '请输入备注名'
|
|
||||||
})
|
|
||||||
|
|
||||||
await contactApi.updateContact(contactId.value, { remark_name: value })
|
|
||||||
contact.value.remark_name = value
|
|
||||||
toast.success('修改成功')
|
|
||||||
} catch (e: any) {
|
|
||||||
if (e !== 'cancel') {
|
|
||||||
toast.error('修改失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function toggleTop() {
|
|
||||||
if (!contact.value) return
|
|
||||||
try {
|
|
||||||
await contactApi.updateContact(contactId.value, { is_top: contact.value.is_top })
|
|
||||||
toast.success(contact.value.is_top ? '已置顶' : '已取消置顶')
|
|
||||||
} catch {
|
|
||||||
contact.value.is_top = !contact.value.is_top
|
|
||||||
toast.error('操作失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function toggleMute() {
|
|
||||||
if (!contact.value) return
|
|
||||||
try {
|
|
||||||
await contactApi.updateContact(contactId.value, { is_muted: contact.value.is_muted })
|
|
||||||
toast.success(contact.value.is_muted ? '已开启免打扰' : '已关闭免打扰')
|
|
||||||
} catch {
|
|
||||||
contact.value.is_muted = !contact.value.is_muted
|
|
||||||
toast.error('操作失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function toggleBlock() {
|
|
||||||
if (!contact.value) return
|
|
||||||
try {
|
|
||||||
await contactApi.updateContact(contactId.value, { is_blocked: contact.value.is_blocked })
|
|
||||||
toast.success(contact.value.is_blocked ? '已加入黑名单' : '已移出黑名单')
|
|
||||||
} catch {
|
|
||||||
contact.value.is_blocked = !contact.value.is_blocked
|
|
||||||
toast.error('操作失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteFriend() {
|
|
||||||
try {
|
|
||||||
await messageBox.confirm({
|
|
||||||
title: '提示',
|
|
||||||
msg: '确定要删除该好友吗?删除后将无法恢复'
|
|
||||||
})
|
|
||||||
|
|
||||||
await contactApi.deleteContact(contactId.value)
|
|
||||||
toast.success('删除成功')
|
|
||||||
setTimeout(() => {
|
|
||||||
uni.navigateBack()
|
|
||||||
}, 1000)
|
|
||||||
} catch (e: any) {
|
|
||||||
if (e !== 'cancel') {
|
|
||||||
toast.error('删除失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.detail-page {
|
/* 高级灰配色 */
|
||||||
|
.detail-container {
|
||||||
|
--bg-base: #F5F7FA;
|
||||||
|
--bg-card: #FFFFFF;
|
||||||
|
--text-main: #1D1D1F;
|
||||||
|
--text-sub: #86909C;
|
||||||
|
--primary: #4F46E5;
|
||||||
|
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: var(--bg-page);
|
background: var(--bg-base);
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&.theme-dark {
|
||||||
|
--bg-base: #121212; /* 深度灰 */
|
||||||
|
--bg-card: #1C1C1E; /* 卡片灰 */
|
||||||
|
--text-main: #F7F8FA;
|
||||||
|
--text-sub: #888;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-state {
|
.profile-header {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 600rpx;
|
||||||
|
z-index: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.blur-bg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
filter: blur(50px);
|
||||||
|
transform: scale(1.2);
|
||||||
|
opacity: 0.8;
|
||||||
|
.theme-dark & { opacity: 0.4; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradient-mask {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: linear-gradient(to bottom, transparent 0%, var(--bg-base) 90%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
height: 100vh;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-wrapper {
|
||||||
|
padding: 0 32rpx;
|
||||||
|
padding-top: calc(180rpx + var(--status-bar-height));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 身份卡片 */
|
||||||
|
.identity-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
margin-bottom: 60rpx;
|
||||||
padding: 100rpx;
|
|
||||||
color: var(--text-tertiary);
|
|
||||||
gap: 20rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-section {
|
.avatar-wrapper {
|
||||||
display: flex;
|
box-shadow: 0 16rpx 40rpx rgba(0,0,0,0.1);
|
||||||
align-items: center;
|
border-radius: 50%;
|
||||||
padding: 40rpx 30rpx;
|
}
|
||||||
background: var(--bg-content);
|
|
||||||
margin-bottom: 20rpx;
|
|
||||||
|
|
||||||
.avatar-placeholder {
|
.info-block {
|
||||||
width: 140rpx;
|
margin-top: 30rpx;
|
||||||
height: 140rpx;
|
text-align: center;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
.user-name {
|
||||||
justify-content: center;
|
font-size: 48rpx;
|
||||||
border-radius: 12rpx;
|
font-weight: 800;
|
||||||
color: #fff;
|
color: var(--text-main);
|
||||||
font-size: 56rpx;
|
margin-bottom: 16rpx;
|
||||||
font-weight: 600;
|
letter-spacing: -1rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
|
||||||
|
.meta-tag {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bio-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: var(--text-main);
|
||||||
|
opacity: 0.6;
|
||||||
|
max-width: 500rpx;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-info {
|
/* 设置组 */
|
||||||
margin-left: 30rpx;
|
.settings-group {
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
|
||||||
.name {
|
.group-label {
|
||||||
display: block;
|
|
||||||
font-size: 40rpx;
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 8rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.id {
|
|
||||||
display: block;
|
|
||||||
font-size: 26rpx;
|
font-size: 26rpx;
|
||||||
color: var(--text-tertiary);
|
color: var(--text-sub);
|
||||||
}
|
margin-bottom: 20rpx;
|
||||||
}
|
margin-left: 12rpx;
|
||||||
|
font-weight: 600;
|
||||||
.actions-section {
|
|
||||||
padding: 60rpx 30rpx;
|
|
||||||
padding-bottom: calc(60rpx + env(safe-area-inset-bottom));
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 24rpx;
|
|
||||||
|
|
||||||
.action-row {
|
|
||||||
display: flex;
|
|
||||||
gap: 20rpx;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex-btn {
|
:deep(.wd-cell-group) {
|
||||||
flex: 1;
|
border-radius: 32rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--bg-card);
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
: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); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-wrap {
|
||||||
|
width: 60rpx;
|
||||||
|
height: 60rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 8rpx;
|
margin-right: 24rpx;
|
||||||
|
|
||||||
|
&.color-blue { background: #007AFF; }
|
||||||
|
&.color-orange { background: #FF9500; }
|
||||||
|
&.color-purple { background: #5856D6; }
|
||||||
|
&.color-red { background: #FF3B30; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cell-text {
|
||||||
|
color: var(--text-sub);
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.spacer { height: 300rpx; }
|
||||||
|
|
||||||
|
/* 底部操作台 */
|
||||||
|
.floating-dock {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
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);
|
||||||
|
z-index: 100;
|
||||||
|
|
||||||
|
.theme-dark & { border-top-color: rgba(255,255,255,0.05); }
|
||||||
|
|
||||||
|
.dock-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 32rpx;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
|
||||||
|
.dock-btn {
|
||||||
|
height: 110rpx;
|
||||||
|
border-radius: 36rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:active { transform: scale(0.96); }
|
||||||
|
|
||||||
|
&.primary {
|
||||||
|
flex: 2;
|
||||||
|
background: var(--primary);
|
||||||
|
color: #fff;
|
||||||
|
gap: 16rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 34rpx;
|
||||||
|
box-shadow: 0 10rpx 24rpx rgba(79, 70, 229, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.secondary {
|
||||||
|
flex: 1;
|
||||||
|
background: rgba(0,0,0,0.04);
|
||||||
|
color: var(--text-main);
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-loading {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,135 +1,138 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="groups-page">
|
<wd-config-provider :theme="isDark ? 'dark' : 'light'">
|
||||||
<view v-if="loading" class="loading-state">
|
<view class="groups-page" :class="{ dark: isDark }">
|
||||||
<wd-loading />
|
<app-nav-bar title="我的群聊" custom-style="background: transparent;" />
|
||||||
<text>加载中...</text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<wd-status-tip v-else-if="groups.length === 0" tip="暂无群聊" />
|
<view class="content-body">
|
||||||
|
<view v-if="loading" class="loading-state">
|
||||||
|
<wd-loading />
|
||||||
|
<text>加载中...</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
<template v-else>
|
<wd-status-tip v-else-if="groups.length === 0" image="content" tip="暂无群聊" />
|
||||||
<wd-cell
|
|
||||||
v-for="group in groups"
|
<view v-else class="group-list">
|
||||||
:key="group.room_id"
|
<!-- 使用 block 遍历 -->
|
||||||
:title="group.room_name"
|
<view
|
||||||
:label="getCategoryText(group.category)"
|
v-for="group in groups"
|
||||||
is-link
|
:key="group.room_id"
|
||||||
@click="goChat(group)"
|
class="group-card"
|
||||||
>
|
@click="goChat(group)"
|
||||||
<template #icon>
|
>
|
||||||
<wd-img
|
<view class="card-left">
|
||||||
v-if="group.room_avatar"
|
<wd-img
|
||||||
:src="group.room_avatar"
|
v-if="group.room_avatar"
|
||||||
width="80rpx"
|
:src="group.room_avatar"
|
||||||
height="80rpx"
|
width="100rpx"
|
||||||
radius="8rpx"
|
height="100rpx"
|
||||||
custom-style="margin-right: 24rpx;"
|
radius="20rpx"
|
||||||
/>
|
/>
|
||||||
<view v-else class="avatar-placeholder" :style="{ background: generateColor(group.room_name || '') }">
|
<view v-else class="avatar-placeholder" :style="{ background: generateColor(group.room_name || '') }">
|
||||||
{{ group.room_name?.charAt(0) || '群' }}
|
{{ group.room_name?.charAt(0) || '群' }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="card-right">
|
||||||
|
<view class="row-1">
|
||||||
|
<text class="group-name text-ellipsis">{{ group.room_name }}</text>
|
||||||
|
<view class="tag" :class="group.category">
|
||||||
|
{{ getCategoryText(group.category) }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<wd-icon name="arrow-right" size="28rpx" color="var(--text-tertiary)" />
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</view>
|
||||||
</wd-cell>
|
</view>
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- 创建群聊按钮 -->
|
<!-- 悬浮创建按钮 -->
|
||||||
<view class="create-section" @click="goCreate">
|
<view class="fab-create" @click="goCreate">
|
||||||
<wd-icon name="add-circle" size="40rpx" color="var(--color-primary)" />
|
<wd-icon name="add" size="50rpx" color="#fff" />
|
||||||
<text>创建群聊</text>
|
<text>创建</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</wd-config-provider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import * as roomApi from '@/api/modules/room'
|
import * as roomApi from '@/api/modules/room'
|
||||||
import { generateColor } from '@/utils/format'
|
import { generateColor } from '@/utils/format'
|
||||||
|
import { useTheme } from '@/composables/useTheme'
|
||||||
|
|
||||||
interface GroupItem {
|
// --- 逻辑保持不变 ---
|
||||||
room_id: string
|
interface GroupItem { room_id: string; room_name: string; room_avatar: string; category: 'joined' | 'created' | 'managed' }
|
||||||
room_name: string
|
const { isDark } = useTheme(); const loading = ref(true); const groups = ref<GroupItem[]>([]);
|
||||||
room_avatar: string
|
onMounted(async () => { await loadGroups() });
|
||||||
category: 'joined' | 'created' | 'managed'
|
async function loadGroups() { loading.value = true; try { groups.value = await roomApi.getUserGroups() } catch (error) { console.error('加载群聊列表失败:', error) } finally { loading.value = false } }
|
||||||
}
|
function getCategoryText(category: string) { switch (category) { case 'created': return '我创建的'; case 'managed': return '我管理的'; default: return '我加入的' } }
|
||||||
|
function goChat(group: GroupItem) { uni.navigateTo({ url: `/pages/chat/index?roomId=${group.room_id}&name=${encodeURIComponent(group.room_name)}` }) }
|
||||||
const loading = ref(true)
|
function goCreate() { uni.navigateTo({ url: '/pages/group/create' }) }
|
||||||
const groups = ref<GroupItem[]>([])
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await loadGroups()
|
|
||||||
})
|
|
||||||
|
|
||||||
async function loadGroups() {
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
groups.value = await roomApi.getUserGroups()
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载群聊列表失败:', error)
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCategoryText(category: string) {
|
|
||||||
switch (category) {
|
|
||||||
case 'created':
|
|
||||||
return '我创建的'
|
|
||||||
case 'managed':
|
|
||||||
return '我管理的'
|
|
||||||
default:
|
|
||||||
return '我加入的'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function goChat(group: GroupItem) {
|
|
||||||
uni.navigateTo({
|
|
||||||
url: `/pages/chat/index?roomId=${group.room_id}&name=${encodeURIComponent(group.room_name)}`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function goCreate() {
|
|
||||||
uni.navigateTo({ url: '/pages/group/create' })
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.groups-page {
|
.groups-page {
|
||||||
|
--bg-page: #f5f7fa;
|
||||||
|
--bg-surface: #ffffff;
|
||||||
|
--text-primary: #1d1d1f;
|
||||||
|
--text-tertiary: #9ca3af;
|
||||||
|
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: var(--bg-page);
|
background: var(--bg-page);
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-state {
|
.groups-page.dark {
|
||||||
display: flex;
|
--bg-page: #1c1c1e;
|
||||||
flex-direction: column;
|
--bg-surface: #2c2c2e;
|
||||||
align-items: center;
|
--text-primary: #f2f2f7;
|
||||||
justify-content: center;
|
|
||||||
padding: 100rpx;
|
|
||||||
color: var(--text-tertiary);
|
|
||||||
gap: 20rpx;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar-placeholder {
|
.content-body { padding: 30rpx; padding-bottom: 160rpx; }
|
||||||
width: 80rpx;
|
|
||||||
height: 80rpx;
|
.loading-state { padding: 100rpx; display: flex; flex-direction: column; align-items: center; gap: 20rpx; color: var(--text-tertiary); }
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
.group-card {
|
||||||
justify-content: center;
|
background: var(--bg-surface);
|
||||||
border-radius: 8rpx;
|
border-radius: 24rpx;
|
||||||
margin-right: 24rpx;
|
padding: 24rpx;
|
||||||
color: #fff;
|
display: flex; align-items: center;
|
||||||
font-size: 32rpx;
|
margin-bottom: 20rpx;
|
||||||
font-weight: 600;
|
transition: transform 0.1s;
|
||||||
|
|
||||||
|
&:active { transform: scale(0.98); }
|
||||||
|
|
||||||
|
.card-left { margin-right: 24rpx; }
|
||||||
|
|
||||||
|
.avatar-placeholder {
|
||||||
|
width: 100rpx; height: 100rpx; border-radius: 20rpx; display: flex; align-items: center; justify-content: center;
|
||||||
|
color: #fff; font-size: 36rpx; font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-right {
|
||||||
|
flex: 1; display: flex; align-items: center; justify-content: space-between;
|
||||||
|
|
||||||
|
.row-1 { display: flex; flex-direction: column; gap: 8rpx; }
|
||||||
|
|
||||||
|
.group-name { font-size: 32rpx; font-weight: 600; color: var(--text-primary); max-width: 400rpx; }
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
align-self: flex-start; font-size: 20rpx; padding: 2rpx 10rpx; border-radius: 8rpx;
|
||||||
|
&.created { background: rgba(79, 70, 229, 0.1); color: #4f46e5; }
|
||||||
|
&.managed { background: rgba(245, 158, 11, 0.1); color: #f59e0b; }
|
||||||
|
&.joined { background: rgba(0,0,0,0.05); color: var(--text-tertiary); .dark & { background: rgba(255,255,255,0.1); } }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-section {
|
.fab-create {
|
||||||
display: flex;
|
position: fixed; bottom: 80rpx; right: 40rpx;
|
||||||
align-items: center;
|
background: #4f46e5; color: #fff;
|
||||||
justify-content: center;
|
height: 90rpx; padding: 0 40rpx; border-radius: 45rpx;
|
||||||
gap: 12rpx;
|
display: flex; align-items: center; gap: 12rpx;
|
||||||
padding: 28rpx;
|
box-shadow: 0 8rpx 24rpx rgba(79, 70, 229, 0.4);
|
||||||
background: var(--bg-content);
|
font-size: 28rpx; font-weight: 600;
|
||||||
margin-top: 20rpx;
|
z-index: 99;
|
||||||
color: var(--color-primary);
|
|
||||||
font-size: 32rpx;
|
&:active { transform: scale(0.95); }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,31 +2,27 @@
|
|||||||
<wd-config-provider :theme="isDark ? 'dark' : 'light'">
|
<wd-config-provider :theme="isDark ? 'dark' : 'light'">
|
||||||
<view class="page-container" :class="{ dark: isDark }">
|
<view class="page-container" :class="{ dark: isDark }">
|
||||||
|
|
||||||
<!-- 顶部固定区域 -->
|
<!-- 头部固定区域 -->
|
||||||
<view class="fixed-header">
|
<view class="fixed-header">
|
||||||
<!-- 导航栏 -->
|
|
||||||
<view class="nav-bar">
|
<view class="nav-bar">
|
||||||
<text class="nav-title">通讯录</text>
|
<text class="nav-title">通讯录</text>
|
||||||
<view class="nav-actions">
|
<!-- 修复:添加好友按钮 -->
|
||||||
<view class="icon-btn" @click="goAddFriend">
|
<view class="icon-btn" @click="goAddFriend">
|
||||||
<wd-icon name="add-user" size="40rpx" />
|
<wd-icon name="add-user" size="48rpx" color="var(--text-primary)" />
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 搜索 -->
|
|
||||||
<view class="search-box-wrap">
|
<view class="search-box-wrap">
|
||||||
<wd-search
|
<wd-search
|
||||||
v-model="searchKeyword"
|
v-model="searchKeyword"
|
||||||
placeholder="搜索联系人/群聊"
|
placeholder="搜索联系人/群聊"
|
||||||
disabled
|
disabled
|
||||||
hide-cancel
|
hide-cancel
|
||||||
custom-class="custom-search"
|
custom-style="background: var(--bg-surface);"
|
||||||
@click="goSearch"
|
@click="goSearch"
|
||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 胶囊式 Tab 切换 (视觉升级) -->
|
|
||||||
<view class="tabs-wrapper">
|
<view class="tabs-wrapper">
|
||||||
<view class="segmented-control">
|
<view class="segmented-control">
|
||||||
<view
|
<view
|
||||||
@@ -38,12 +34,11 @@
|
|||||||
>
|
>
|
||||||
{{ tab.label }}
|
{{ tab.label }}
|
||||||
</view>
|
</view>
|
||||||
<!-- 滑块动画可通过 CSS transform 实现,这里简化为 active 类 -->
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 主要内容滚动区 -->
|
<!-- 滚动内容 -->
|
||||||
<scroll-view
|
<scroll-view
|
||||||
class="main-scroll"
|
class="main-scroll"
|
||||||
scroll-y
|
scroll-y
|
||||||
@@ -51,44 +46,34 @@
|
|||||||
:refresher-triggered="refreshing"
|
:refresher-triggered="refreshing"
|
||||||
@refresherrefresh="onRefresh"
|
@refresherrefresh="onRefresh"
|
||||||
>
|
>
|
||||||
<!-- 功能入口 (仅在分组Tab显示) -->
|
<!-- 修复:顶部间距,防止Grid被遮挡 -->
|
||||||
|
<view class="scroll-spacer"></view>
|
||||||
|
|
||||||
|
<!-- 功能 Grid -->
|
||||||
<view v-if="activeTab === 'groups'" class="feature-grid">
|
<view v-if="activeTab === 'groups'" class="feature-grid">
|
||||||
<view class="feature-card" @click="goFriendRequests">
|
<view class="feature-card" @click="goFriendRequests">
|
||||||
<view class="icon-wrap is-orange">
|
<view class="icon-wrap is-orange"><wd-icon name="user-add" size="36rpx" color="#fff" /></view>
|
||||||
<wd-icon name="user-add" size="36rpx" color="#fff" />
|
|
||||||
</view>
|
|
||||||
<view class="text-wrap">
|
<view class="text-wrap">
|
||||||
<text class="feature-title">新朋友</text>
|
<text class="feature-title">新朋友</text>
|
||||||
<wd-badge v-if="friendRequestCount > 0" :value="friendRequestCount" type="danger" />
|
<wd-badge v-if="friendRequestCount > 0" :value="friendRequestCount" type="danger" />
|
||||||
</view>
|
</view>
|
||||||
<wd-icon name="arrow-right" size="24rpx" color="var(--text-tertiary)" />
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="feature-card" @click="goGroupNotify">
|
<view class="feature-card" @click="goGroupNotify">
|
||||||
<view class="icon-wrap is-blue">
|
<view class="icon-wrap is-blue"><wd-icon name="bell" size="36rpx" color="#fff" /></view>
|
||||||
<wd-icon name="bell" size="36rpx" color="#fff" />
|
<view class="text-wrap"><text class="feature-title">群通知</text></view>
|
||||||
</view>
|
|
||||||
<view class="text-wrap">
|
|
||||||
<text class="feature-title">群通知</text>
|
|
||||||
</view>
|
|
||||||
<wd-icon name="arrow-right" size="24rpx" color="var(--text-tertiary)" />
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 列表内容区 -->
|
<!-- 列表内容 -->
|
||||||
<view class="list-container">
|
<view class="list-container">
|
||||||
<view v-if="loading" class="loading-box">
|
<view v-if="loading" class="loading-box"><wd-loading size="40rpx" /></view>
|
||||||
<wd-loading size="40rpx" />
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 分组 Tab -->
|
<!-- 1. 分组 Tab -->
|
||||||
<template v-else-if="activeTab === 'groups'">
|
<template v-else-if="activeTab === 'groups'">
|
||||||
<!-- 这里的实现逻辑保持,但样式升级 -->
|
|
||||||
<view class="group-section-header">
|
<view class="group-section-header">
|
||||||
<text class="section-title">我的分组</text>
|
<text class="section-title">我的分组</text>
|
||||||
<view class="add-group-btn" @click="showCreateGroupModal = true">
|
<view class="add-group-btn" @click="showCreateGroupModal = true">
|
||||||
<wd-icon name="add" size="24rpx" />
|
<wd-icon name="add" size="24rpx" /> <text>新建</text>
|
||||||
<text>新建</text>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -100,13 +85,7 @@
|
|||||||
<text class="collapse-count">{{ ungroupedContacts.length }}</text>
|
<text class="collapse-count">{{ ungroupedContacts.length }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view v-show="!collapsedIds.includes(0)" class="collapse-content">
|
<view v-show="!collapsedIds.includes(0)" class="collapse-content">
|
||||||
<view
|
<view v-for="contact in ungroupedContacts" :key="contact.id" class="contact-cell" @click="goContactDetail(contact)" @longpress="handleContactLongPress(contact)">
|
||||||
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="12rpx" />
|
||||||
<view class="cell-body">
|
<view class="cell-body">
|
||||||
<text class="cell-title">{{ contact.remark_name || contact.user?.name }}</text>
|
<text class="cell-title">{{ contact.remark_name || contact.user?.name }}</text>
|
||||||
@@ -116,7 +95,7 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 自定义分组 (逻辑同上,样式复用) -->
|
<!-- 自定义分组 -->
|
||||||
<view v-for="group in contactGroups" :key="group.id" class="collapse-item">
|
<view v-for="group in contactGroups" :key="group.id" class="collapse-item">
|
||||||
<view class="collapse-header" @click="toggleCollapse(group.id)" @longpress="handleGroupLongPress(group)">
|
<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)" />
|
<wd-icon :name="collapsedIds.includes(group.id) ? 'arrow-right' : 'arrow-down'" size="24rpx" color="var(--text-tertiary)" />
|
||||||
@@ -135,7 +114,7 @@
|
|||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 好友 Tab (A-Z) -->
|
<!-- 2. 好友 Tab (A-Z) -->
|
||||||
<template v-else-if="activeTab === 'friends'">
|
<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">
|
||||||
<view class="alpha-header">{{ group.letter }}</view>
|
<view class="alpha-header">{{ group.letter }}</view>
|
||||||
@@ -150,46 +129,67 @@
|
|||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 群聊 Tab 样式复用 collapse-item 结构 -->
|
<!-- 3. 群聊 Tab (修复:显示所有群组) -->
|
||||||
<template v-else-if="activeTab === 'chats'">
|
<template v-else-if="activeTab === 'chats'">
|
||||||
<!-- 示例:我创建的群 -->
|
<!-- 我创建的 -->
|
||||||
<view v-if="createdGroups.length" class="collapse-item">
|
<view v-if="createdGroups.length" class="collapse-item">
|
||||||
<view class="collapse-header" @click="toggleCollapse('created')">
|
<view class="collapse-header" @click="toggleCollapse('created')">
|
||||||
|
<wd-icon :name="collapsedIds.includes('created') ? 'arrow-right' : 'arrow-down'" size="24rpx" color="var(--text-tertiary)" />
|
||||||
<text class="collapse-title">我创建的群</text>
|
<text class="collapse-title">我创建的群</text>
|
||||||
<text class="collapse-count">{{ createdGroups.length }}</text>
|
<text class="collapse-count">{{ createdGroups.length }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view v-show="!collapsedIds.includes('created')" class="collapse-content">
|
<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)">
|
<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="12rpx" />
|
||||||
<view class="cell-body">
|
<view class="cell-body"><text class="cell-title">{{ g.room_name }}</text></view>
|
||||||
<text class="cell-title">{{ g.room_name }}</text>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 我管理的 -->
|
||||||
|
<view v-if="managedGroups.length" class="collapse-item">
|
||||||
|
<view class="collapse-header" @click="toggleCollapse('managed')">
|
||||||
|
<wd-icon :name="collapsedIds.includes('managed') ? 'arrow-right' : 'arrow-down'" size="24rpx" color="var(--text-tertiary)" />
|
||||||
|
<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" />
|
||||||
|
<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 class="collapse-header" @click="toggleCollapse('joined')">
|
||||||
|
<wd-icon :name="collapsedIds.includes('joined') ? 'arrow-right' : 'arrow-down'" size="24rpx" color="var(--text-tertiary)" />
|
||||||
|
<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" />
|
||||||
|
<view class="cell-body"><text class="cell-title">{{ g.room_name }}</text></view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<!-- 其他群组类别同理 -->
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<view class="safe-area-spacer"></view>
|
<view class="safe-area-spacer"></view>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
|
|
||||||
<!-- 字母索引 (悬浮) -->
|
<!-- 索引条 -->
|
||||||
<view v-if="activeTab === 'friends'" class="index-bar">
|
<view v-if="activeTab === 'friends'" class="index-bar">
|
||||||
<view
|
<view v-for="l in letters" :key="l" class="index-item" @click.stop="scrollToLetter(l)">{{ l }}</view>
|
||||||
v-for="l in letters"
|
|
||||||
:key="l"
|
|
||||||
class="index-item"
|
|
||||||
@click.stop="scrollToLetter(l)"
|
|
||||||
>{{ l }}</view>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 保持所有弹窗组件 (ActionSheets, Popups) 逻辑不变 -->
|
<!-- 弹窗组件 -->
|
||||||
<wd-action-sheet v-model="showContactActions" :actions="contactActionItems" @select="onContactActionSelect" cancel-text="取消" />
|
<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="showGroupActions" :actions="groupActionItems" @select="onGroupActionSelect" cancel-text="取消" />
|
||||||
<wd-action-sheet v-model="showMoveGroupModal" :actions="moveGroupActions" @select="onMoveGroupSelect" cancel-text="取消" />
|
<wd-action-sheet v-model="showMoveGroupModal" :actions="moveGroupActions" @select="onMoveGroupSelect" cancel-text="取消" />
|
||||||
|
|
||||||
<!-- 弹窗样式也可以通过 global CSS 优化,这里复用 wd-popup -->
|
|
||||||
<wd-popup v-model="showCreateGroupModal" custom-style="border-radius: 24rpx; width: 80%; padding: 40rpx; background: var(--bg-surface);">
|
<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-content">
|
||||||
<view class="modal-title">新建分组</view>
|
<view class="modal-title">新建分组</view>
|
||||||
@@ -200,10 +200,7 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</wd-popup>
|
</wd-popup>
|
||||||
|
|
||||||
<!-- 其他 Popup 复用上述结构 -->
|
|
||||||
<wd-popup v-model="showRenameGroupModal" custom-style="border-radius: 24rpx; width: 80%; padding: 40rpx; background: var(--bg-surface);">
|
<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-content">
|
||||||
<view class="modal-title">重命名分组</view>
|
<view class="modal-title">重命名分组</view>
|
||||||
<wd-input v-model="renameGroupName" placeholder="请输入新名称" clearable custom-style="margin: 30rpx 0;" />
|
<wd-input v-model="renameGroupName" placeholder="请输入新名称" clearable custom-style="margin: 30rpx 0;" />
|
||||||
@@ -213,9 +210,7 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</wd-popup>
|
</wd-popup>
|
||||||
|
|
||||||
<wd-popup v-model="showRemarkModal" custom-style="border-radius: 24rpx; width: 80%; padding: 40rpx; background: var(--bg-surface);">
|
<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-content">
|
||||||
<view class="modal-title">修改备注</view>
|
<view class="modal-title">修改备注</view>
|
||||||
<wd-input v-model="remarkName" placeholder="请输入备注名" clearable custom-style="margin: 30rpx 0;" />
|
<wd-input v-model="remarkName" placeholder="请输入备注名" clearable custom-style="margin: 30rpx 0;" />
|
||||||
@@ -245,84 +240,18 @@ import AppAvatar from '@/components/common/AppAvatar.vue'
|
|||||||
import AppTabBar from '@/components/common/AppTabBar.vue'
|
import AppTabBar from '@/components/common/AppTabBar.vue'
|
||||||
import type { Contact, ContactGroup } from '@/types/api'
|
import type { Contact, ContactGroup } from '@/types/api'
|
||||||
|
|
||||||
// 类型定义补全
|
// --- 逻辑完全不变 ---
|
||||||
interface GroupChat {
|
interface GroupChat { room_id: string; room_type: string; room_name: string; room_avatar: string; category: 'joined' | 'created' | 'managed'; }
|
||||||
room_id: string; room_type: string; room_name: string; room_avatar: string; category: 'joined' | 'created' | 'managed';
|
const contactStore = useContactStore(); const chatStore = useChatStore(); const { isDark } = useTheme(); const toast = useToast(); const messageBox = useMessage();
|
||||||
}
|
const tabs = [{ key: 'groups', label: '分组' }, { key: 'friends', label: '好友' }, { key: 'chats', label: '群聊' }]; const activeTab = ref('groups'); const searchKeyword = ref(''); const refreshing = ref(false); const loading = ref(false); const collapsedIds = ref<(number | string)[]>([]); const contactGroups = ref<ContactGroup[]>([]); const groupChats = ref<GroupChat[]>([]); const showContactActions = ref(false); const selectedContact = ref<Contact | null>(null); const showMoveGroupModal = ref(false); const showRemarkModal = ref(false); const remarkName = ref(''); const savingRemark = ref(false); const showGroupActions = ref(false); const selectedGroup = ref<ContactGroup | null>(null); const showCreateGroupModal = ref(false); const showRenameGroupModal = ref(false); const newGroupName = ref(''); const renameGroupName = ref(''); const creating = ref(false); const renaming = ref(false);
|
||||||
|
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 contactStore = useContactStore()
|
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 chatStore = useChatStore()
|
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 { isDark } = useTheme()
|
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 toast = useToast()
|
onMounted(() => loadData()); onShow(() => loadFriendRequests());
|
||||||
const messageBox = useMessage()
|
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 } }
|
||||||
|
|
||||||
// 保持全部原有逻辑变量和方法不变
|
|
||||||
const tabs = [{ key: 'groups', label: '分组' }, { key: 'friends', label: '好友' }, { key: 'chats', label: '群聊' }]
|
|
||||||
const activeTab = ref('groups')
|
|
||||||
const searchKeyword = ref('')
|
|
||||||
const refreshing = ref(false)
|
|
||||||
const loading = ref(false)
|
|
||||||
const collapsedIds = ref<(number | string)[]>([])
|
|
||||||
const contactGroups = ref<ContactGroup[]>([])
|
|
||||||
const groupChats = ref<GroupChat[]>([])
|
|
||||||
const showContactActions = ref(false)
|
|
||||||
const selectedContact = ref<Contact | null>(null)
|
|
||||||
const showMoveGroupModal = ref(false)
|
|
||||||
const showRemarkModal = ref(false)
|
|
||||||
const remarkName = ref('')
|
|
||||||
const savingRemark = ref(false)
|
|
||||||
const showGroupActions = ref(false)
|
|
||||||
const selectedGroup = ref<ContactGroup | null>(null)
|
|
||||||
const showCreateGroupModal = ref(false)
|
|
||||||
const showRenameGroupModal = ref(false)
|
|
||||||
const newGroupName = ref('')
|
|
||||||
const renameGroupName = ref('')
|
|
||||||
const creating = ref(false)
|
|
||||||
const renaming = ref(false)
|
|
||||||
|
|
||||||
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
|
|
||||||
})
|
|
||||||
|
|
||||||
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 {} }
|
async function loadFriendRequests() { try { const requests = await contactApi.getFriendRequests(); contactStore.setFriendRequests(requests) } catch {} }
|
||||||
async function onRefresh() { refreshing.value = true; await loadData(); await loadFriendRequests(); refreshing.value = false }
|
async function onRefresh() { refreshing.value = true; await loadData(); await loadFriendRequests(); refreshing.value = false }
|
||||||
|
|
||||||
function toggleCollapse(id: number | string) { const idx = collapsedIds.value.indexOf(id); idx === -1 ? collapsedIds.value.push(id) : collapsedIds.value.splice(idx, 1) }
|
function toggleCollapse(id: number | string) { const idx = collapsedIds.value.indexOf(id); idx === -1 ? collapsedIds.value.push(id) : collapsedIds.value.splice(idx, 1) }
|
||||||
function goSearch() { uni.navigateTo({ url: '/pages/search/index' }) }
|
function goSearch() { uni.navigateTo({ url: '/pages/search/index' }) }
|
||||||
function goAddFriend() { uni.navigateTo({ url: '/pages/contact/add' }) }
|
function goAddFriend() { uni.navigateTo({ url: '/pages/contact/add' }) }
|
||||||
@@ -330,14 +259,12 @@ function goFriendRequests() { uni.navigateTo({ url: '/pages/contact/requests' })
|
|||||||
function goGroupNotify() { uni.navigateTo({ url: '/pages/group/notify' }) }
|
function goGroupNotify() { uni.navigateTo({ url: '/pages/group/notify' }) }
|
||||||
function goContactDetail(contact: Contact) { uni.navigateTo({ url: `/pages/contact/detail?id=${contact.id}` }) }
|
function goContactDetail(contact: Contact) { uni.navigateTo({ url: `/pages/contact/detail?id=${contact.id}` }) }
|
||||||
function goGroupChat(group: GroupChat) { uni.navigateTo({ url: `/pages/chat/index?roomId=${group.room_id}&name=${encodeURIComponent(group.room_name || '群聊')}` }) }
|
function goGroupChat(group: GroupChat) { uni.navigateTo({ url: `/pages/chat/index?roomId=${group.room_id}&name=${encodeURIComponent(group.room_name || '群聊')}` }) }
|
||||||
function scrollToLetter(letter: string) { /* Scroll logic */ console.log(letter) }
|
function scrollToLetter(letter: string) { console.log(letter) }
|
||||||
|
|
||||||
function handleContactLongPress(c: Contact) { selectedContact.value = c; remarkName.value = c.remark_name || ''; showContactActions.value = true }
|
function handleContactLongPress(c: Contact) { selectedContact.value = c; remarkName.value = c.remark_name || ''; showContactActions.value = true }
|
||||||
function onContactActionSelect(a: { value: string }) { showContactActions.value = false; if(a.value === 'remark') showRemarkModal.value = true; else if(a.value === 'move') showMoveGroupModal.value = true; else if(a.value === 'delete') handleDeleteContact() }
|
function onContactActionSelect(a: { value: string }) { showContactActions.value = false; if(a.value === 'remark') showRemarkModal.value = true; else if(a.value === 'move') showMoveGroupModal.value = true; else if(a.value === 'delete') handleDeleteContact() }
|
||||||
async function handleSaveRemark() { if(!selectedContact.value) return; savingRemark.value=true; try { await contactApi.updateContact(selectedContact.value.id.toString(), {remark_name: remarkName.value}); const idx = chatStore.contacts.findIndex(c => c.id === selectedContact.value?.id); if(idx>-1) chatStore.contacts[idx].remark_name = remarkName.value; showRemarkModal.value=false; toast.success('备注已修改') } catch(e){toast.error('失败')} finally {savingRemark.value=false} }
|
async function handleSaveRemark() { if(!selectedContact.value) return; savingRemark.value=true; try { await contactApi.updateContact(selectedContact.value.id.toString(), {remark_name: remarkName.value}); const idx = chatStore.contacts.findIndex(c => c.id === selectedContact.value?.id); if(idx>-1) chatStore.contacts[idx].remark_name = remarkName.value; showRemarkModal.value=false; toast.success('备注已修改') } catch(e){toast.error('失败')} finally {savingRemark.value=false} }
|
||||||
async function onMoveGroupSelect(a: { value: number }) { if(!selectedContact.value) return; showMoveGroupModal.value=false; try { await contactApi.updateContact(selectedContact.value.id.toString(), {group_id: a.value}); const idx = chatStore.contacts.findIndex(c => c.id === selectedContact.value?.id); if(idx>-1) chatStore.contacts[idx].group_id = a.value; toast.success('已移动') } catch { toast.error('失败') } }
|
async function onMoveGroupSelect(a: { value: number }) { if(!selectedContact.value) return; showMoveGroupModal.value=false; try { await contactApi.updateContact(selectedContact.value.id.toString(), {group_id: a.value}); const idx = chatStore.contacts.findIndex(c => c.id === selectedContact.value?.id); if(idx>-1) chatStore.contacts[idx].group_id = a.value; toast.success('已移动') } catch { toast.error('失败') } }
|
||||||
async function handleDeleteContact() { try { await messageBox.confirm({title:'提示', msg:'确定删除?'}); await contactApi.deleteContact(selectedContact.value!.id.toString()); const idx = chatStore.contacts.findIndex(c=>c.id===selectedContact.value!.id); if(idx>-1) chatStore.contacts.splice(idx,1); toast.success('已删除') } catch {} }
|
async function handleDeleteContact() { try { await messageBox.confirm({title:'提示', msg:'确定删除?'}); await contactApi.deleteContact(selectedContact.value!.id.toString()); const idx = chatStore.contacts.findIndex(c=>c.id===selectedContact.value!.id); if(idx>-1) chatStore.contacts.splice(idx,1); toast.success('已删除') } catch {} }
|
||||||
|
|
||||||
function handleGroupLongPress(g: ContactGroup) { selectedGroup.value=g; renameGroupName.value=g.group_name; showGroupActions.value=true }
|
function handleGroupLongPress(g: ContactGroup) { selectedGroup.value=g; renameGroupName.value=g.group_name; showGroupActions.value=true }
|
||||||
function onGroupActionSelect(a: { value: string }) { showGroupActions.value=false; if(a.value==='rename') showRenameGroupModal.value=true; else if(a.value==='delete') handleDeleteGroup() }
|
function onGroupActionSelect(a: { value: string }) { showGroupActions.value=false; if(a.value==='rename') showRenameGroupModal.value=true; else if(a.value==='delete') handleDeleteGroup() }
|
||||||
async function handleCreateGroup() { if(!newGroupName.value.trim()) return; creating.value=true; try { const ng = await contactApi.createGroup({group_name: newGroupName.value.trim()}); contactGroups.value.push(ng); showCreateGroupModal.value=false; newGroupName.value=''; toast.success('成功') } catch { toast.error('失败') } finally { creating.value=false } }
|
async function handleCreateGroup() { if(!newGroupName.value.trim()) return; creating.value=true; try { const ng = await contactApi.createGroup({group_name: newGroupName.value.trim()}); contactGroups.value.push(ng); showCreateGroupModal.value=false; newGroupName.value=''; toast.success('成功') } catch { toast.error('失败') } finally { creating.value=false } }
|
||||||
@@ -346,7 +273,6 @@ async function handleDeleteGroup() { try { await messageBox.confirm({title:'提
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
/* 共享变量定义 */
|
|
||||||
.page-container {
|
.page-container {
|
||||||
--bg-page: #f7f8fa;
|
--bg-page: #f7f8fa;
|
||||||
--bg-surface: #ffffff;
|
--bg-surface: #ffffff;
|
||||||
@@ -354,7 +280,7 @@ async function handleDeleteGroup() { try { await messageBox.confirm({title:'提
|
|||||||
--text-primary: #1f1f1f;
|
--text-primary: #1f1f1f;
|
||||||
--text-secondary: #6b7280;
|
--text-secondary: #6b7280;
|
||||||
--text-tertiary: #9ca3af;
|
--text-tertiary: #9ca3af;
|
||||||
--border-color: #e5e7eb;
|
--border-color: rgba(0,0,0,0.05);
|
||||||
--brand-color: #4f46e5;
|
--brand-color: #4f46e5;
|
||||||
--tab-bg: #e5e7eb;
|
--tab-bg: #e5e7eb;
|
||||||
--tab-active-bg: #ffffff;
|
--tab-active-bg: #ffffff;
|
||||||
@@ -365,233 +291,86 @@ async function handleDeleteGroup() { try { await messageBox.confirm({title:'提
|
|||||||
}
|
}
|
||||||
|
|
||||||
.page-container.dark {
|
.page-container.dark {
|
||||||
--bg-page: #000000;
|
--bg-page: #1c1c1e; /* Dark Grey */
|
||||||
--bg-surface: #1c1c1e;
|
--bg-surface: #2c2c2e;
|
||||||
--bg-highlight: #2c2c2e;
|
--bg-highlight: #3a3a3c;
|
||||||
--text-primary: #f3f4f6;
|
--text-primary: #f2f2f7;
|
||||||
--text-secondary: #9ca3af;
|
--text-secondary: #aeaeb2;
|
||||||
--text-tertiary: #6b7280;
|
--text-tertiary: #636366;
|
||||||
--border-color: #374151;
|
--border-color: rgba(255,255,255,0.1);
|
||||||
--tab-bg: #2c2c2e;
|
--tab-bg: #2c2c2e;
|
||||||
--tab-active-bg: #636366;
|
--tab-active-bg: #636366;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 顶部固定区域布局 */
|
|
||||||
.fixed-header {
|
.fixed-header {
|
||||||
position: fixed;
|
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
|
||||||
top: 0;
|
background: var(--bg-page);
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 100;
|
|
||||||
background: var(--bg-page); /* 不透明背景 */
|
|
||||||
padding-top: var(--status-bar-height);
|
padding-top: var(--status-bar-height);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-bar {
|
.nav-bar {
|
||||||
height: 88rpx;
|
height: 88rpx; display: flex; align-items: center; justify-content: space-between; padding: 0 32rpx;
|
||||||
display: flex;
|
.nav-title { font-size: 40rpx; font-weight: 700; }
|
||||||
align-items: center;
|
.icon-btn { &:active { opacity: 0.7; } }
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0 32rpx;
|
|
||||||
|
|
||||||
.nav-title {
|
|
||||||
font-size: 40rpx;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 搜索框容器 */
|
.search-box-wrap { padding: 10rpx 24rpx; }
|
||||||
.search-box-wrap {
|
|
||||||
padding: 10rpx 24rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 分段控制器 (iOS Style) */
|
|
||||||
.tabs-wrapper {
|
|
||||||
padding: 16rpx 32rpx 24rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
.tabs-wrapper { padding: 16rpx 32rpx 24rpx; }
|
||||||
.segmented-control {
|
.segmented-control {
|
||||||
background: var(--tab-bg);
|
background: var(--tab-bg); border-radius: 16rpx; padding: 6rpx; display: flex; height: 72rpx;
|
||||||
border-radius: 16rpx;
|
|
||||||
padding: 6rpx;
|
|
||||||
display: flex;
|
|
||||||
height: 72rpx;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.segment-item {
|
.segment-item {
|
||||||
flex: 1;
|
flex: 1; display: flex; align-items: center; justify-content: center;
|
||||||
display: flex;
|
font-size: 28rpx; font-weight: 500; color: var(--text-secondary); border-radius: 12rpx;
|
||||||
align-items: center;
|
transition: all 0.3s;
|
||||||
justify-content: center;
|
&.active { background: var(--tab-active-bg); color: var(--text-primary); font-weight: 600; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.08); }
|
||||||
font-size: 28rpx;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
border-radius: 12rpx;
|
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
z-index: 2;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background: var(--tab-active-bg);
|
|
||||||
color: var(--text-primary);
|
|
||||||
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.08);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 滚动区域 */
|
.main-scroll { margin-top: calc(var(--status-bar-height) + 290rpx); height: calc(100vh - var(--status-bar-height) - 290rpx); }
|
||||||
.main-scroll {
|
|
||||||
/* 计算高度:Full - Header部分。 Header约 (88 + 80 + 112)rpx */
|
|
||||||
margin-top: calc(var(--status-bar-height) + 290rpx);
|
|
||||||
height: calc(100vh - var(--status-bar-height) - 290rpx);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Grid 功能区 */
|
|
||||||
.feature-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 24rpx;
|
|
||||||
padding: 0 32rpx 32rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/* 修复:Grid 顶部垫片 */
|
||||||
|
.scroll-spacer { height: 20rpx; }
|
||||||
|
.feature-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 24rpx; padding: 0 32rpx 32rpx; }
|
||||||
.feature-card {
|
.feature-card {
|
||||||
background: var(--bg-surface);
|
background: var(--bg-surface); padding: 24rpx; border-radius: 20rpx; display: flex; align-items: center; gap: 20rpx;
|
||||||
padding: 24rpx;
|
.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); } }
|
||||||
border-radius: 20rpx;
|
.text-wrap { flex: 1; display: flex; flex-direction: column; .feature-title { font-weight: 600; font-size: 28rpx; } }
|
||||||
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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 列表容器 */
|
|
||||||
.list-container {
|
|
||||||
padding: 0 32rpx;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.list-container { padding: 0 32rpx; }
|
||||||
.group-section-header {
|
.group-section-header {
|
||||||
display: flex;
|
display: flex; align-items: center; justify-content: space-between; margin: 32rpx 0 16rpx;
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin: 32rpx 0 16rpx;
|
|
||||||
|
|
||||||
.section-title { font-size: 32rpx; font-weight: 700; color: var(--text-secondary); }
|
.section-title { font-size: 32rpx; font-weight: 700; color: var(--text-secondary); }
|
||||||
.add-group-btn {
|
.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; }
|
||||||
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-item { margin-bottom: 24rpx; background: var(--bg-surface); border-radius: 20rpx; overflow: hidden; }
|
||||||
.collapse-header {
|
.collapse-header {
|
||||||
padding: 24rpx 24rpx;
|
padding: 24rpx; display: flex; align-items: center; gap: 16rpx; &:active { background: var(--bg-highlight); }
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 16rpx;
|
|
||||||
|
|
||||||
&:active { background: var(--bg-highlight); }
|
|
||||||
|
|
||||||
.collapse-title { flex: 1; font-weight: 600; font-size: 30rpx; }
|
.collapse-title { flex: 1; font-weight: 600; font-size: 30rpx; }
|
||||||
.collapse-count { color: var(--text-tertiary); font-size: 24rpx; }
|
.collapse-count { color: var(--text-tertiary); font-size: 24rpx; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.contact-cell {
|
.contact-cell {
|
||||||
padding: 20rpx 24rpx;
|
padding: 20rpx 24rpx; display: flex; align-items: center; gap: 24rpx; &:active { background: var(--bg-highlight); }
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 24rpx;
|
|
||||||
|
|
||||||
&:active { background: var(--bg-highlight); }
|
|
||||||
|
|
||||||
.cell-body {
|
.cell-body {
|
||||||
flex: 1;
|
flex: 1; display: flex; flex-direction: column; padding-right: 20rpx;
|
||||||
display: flex;
|
&.border-bottom { padding-bottom: 20rpx; border-bottom: 1rpx solid var(--border-color); }
|
||||||
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-title { font-size: 30rpx; font-weight: 500; margin-bottom: 6rpx; }
|
||||||
.cell-desc { font-size: 24rpx; color: var(--text-tertiary); }
|
.cell-desc { font-size: 24rpx; color: var(--text-tertiary); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 字母分组 */
|
.alpha-group { margin-bottom: 30rpx; }
|
||||||
.alpha-group {
|
.alpha-header { font-size: 24rpx; font-weight: 700; color: var(--brand-color); margin-bottom: 12rpx; }
|
||||||
margin-bottom: 30rpx;
|
|
||||||
}
|
|
||||||
.alpha-header {
|
|
||||||
font-size: 24rpx;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--brand-color);
|
|
||||||
margin-bottom: 12rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 字母索引条 */
|
|
||||||
.index-bar {
|
.index-bar {
|
||||||
position: fixed;
|
position: fixed; right: 8rpx; top: 55%; transform: translateY(-50%);
|
||||||
right: 8rpx;
|
background: var(--bg-highlight); border-radius: 20rpx; padding: 16rpx 6rpx; z-index: 101;
|
||||||
top: 55%;
|
.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); } }
|
||||||
transform: translateY(-50%);
|
|
||||||
background: var(--bg-highlight);
|
|
||||||
border-radius: 20rpx;
|
|
||||||
padding: 16rpx 6rpx;
|
|
||||||
z-index: 101;
|
|
||||||
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.1);
|
|
||||||
|
|
||||||
.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); transform: scale(1.2); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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; }
|
.safe-area-spacer { height: 180rpx; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,177 +1,146 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="requests-page" :class="{ dark: isDark }">
|
<wd-config-provider :theme="isDark ? 'dark' : 'light'">
|
||||||
<!-- 导航栏 -->
|
<view class="requests-page" :class="{ dark: isDark }">
|
||||||
<app-nav-bar title="新朋友" />
|
<app-nav-bar title="新朋友" custom-style="background: transparent;" />
|
||||||
|
|
||||||
<view v-if="loading" class="loading-state">
|
|
||||||
<wd-loading />
|
|
||||||
<text>加载中...</text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<wd-status-tip v-else-if="requests.length === 0" tip="暂无好友申请" />
|
<view class="content-body">
|
||||||
|
<view v-if="loading" class="loading-state">
|
||||||
<template v-else>
|
<wd-loading size="50rpx" />
|
||||||
<view
|
<text>加载中...</text>
|
||||||
v-for="item in requests"
|
|
||||||
:key="item.id"
|
|
||||||
class="request-item"
|
|
||||||
>
|
|
||||||
<app-avatar
|
|
||||||
:src="item.from_user?.avatar"
|
|
||||||
:name="item.from_user?.name"
|
|
||||||
:size="96"
|
|
||||||
radius="8rpx"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<view class="request-info">
|
|
||||||
<text class="name">{{ item.from_user?.name || '未知用户' }}</text>
|
|
||||||
<text class="message">{{ item.message || '请求添加您为好友' }}</text>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="request-actions">
|
<wd-status-tip v-else-if="requests.length === 0" image="message" tip="暂无好友申请" />
|
||||||
<template v-if="item.status === 'pending'">
|
|
||||||
<wd-button size="small" plain @click="reject(item)">拒绝</wd-button>
|
<template v-else>
|
||||||
<wd-button size="small" type="primary" @click="accept(item)">同意</wd-button>
|
<view
|
||||||
</template>
|
v-for="item in requests"
|
||||||
<text v-else class="status" :class="item.status">
|
:key="item.id"
|
||||||
{{ item.status === 'accepted' ? '已添加' : '已拒绝' }}
|
class="request-card"
|
||||||
</text>
|
>
|
||||||
</view>
|
<view class="card-left">
|
||||||
|
<app-avatar
|
||||||
|
:src="item.from_user?.avatar"
|
||||||
|
:name="item.from_user?.name"
|
||||||
|
:size="100"
|
||||||
|
radius="20rpx"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="card-content">
|
||||||
|
<view class="info-top">
|
||||||
|
<text class="name">{{ item.from_user?.name || '未知用户' }}</text>
|
||||||
|
<text class="time">刚刚</text> <!-- 这里可以加个时间字段 -->
|
||||||
|
</view>
|
||||||
|
<view class="msg-box">
|
||||||
|
<text class="message">{{ item.message || '请求添加您为好友' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="card-footer">
|
||||||
|
<template v-if="item.status === 'pending'">
|
||||||
|
<view class="btn-group">
|
||||||
|
<view class="action-btn reject" @click="reject(item)">拒绝</view>
|
||||||
|
<view class="action-btn accept" @click="accept(item)">同意</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
<text v-else class="status-text" :class="item.status">
|
||||||
|
{{ item.status === 'accepted' ? '已添加' : '已拒绝' }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
|
||||||
|
|
||||||
<wd-toast />
|
<wd-toast />
|
||||||
</view>
|
</view>
|
||||||
|
</wd-config-provider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { useContactStore } from '@/stores'
|
import { useContactStore } from '@/stores'
|
||||||
import * as contactApi from '@/api/modules/contact'
|
import * as contactApi from '@/api/modules/contact'
|
||||||
import { generateColor } from '@/utils/format'
|
|
||||||
import { resolveImageUrl } from '@/utils/image'
|
|
||||||
import { useToast } from 'wot-design-uni'
|
import { useToast } from 'wot-design-uni'
|
||||||
import { useTheme } from '@/composables/useTheme'
|
import { useTheme } from '@/composables/useTheme'
|
||||||
import AppAvatar from '@/components/common/AppAvatar.vue'
|
import AppAvatar from '@/components/common/AppAvatar.vue'
|
||||||
import type { FriendRequest } from '@/types/api'
|
import type { FriendRequest } from '@/types/api'
|
||||||
|
|
||||||
const contactStore = useContactStore()
|
// --- 逻辑保持不变 ---
|
||||||
const toast = useToast()
|
const contactStore = useContactStore(); const toast = useToast(); const { isDark } = useTheme();
|
||||||
const { isDark } = useTheme()
|
const loading = ref(true); const requests = ref<FriendRequest[]>([]);
|
||||||
|
onMounted(async () => { await loadRequests() });
|
||||||
const loading = ref(true)
|
async function loadRequests() { loading.value = true; try { requests.value = await contactApi.getFriendRequests(); contactStore.setFriendRequests(requests.value) } catch (error) { console.error(error) } finally { loading.value = false } }
|
||||||
const requests = ref<FriendRequest[]>([])
|
async function accept(item: FriendRequest) { try { await contactApi.acceptFriendRequest(item.id); item.status = 'accepted'; contactStore.removeFriendRequest(item.id); toast.success('已添加') } catch { toast.error('操作失败') } }
|
||||||
|
async function reject(item: FriendRequest) { try { await contactApi.rejectFriendRequest(item.id); item.status = 'rejected'; contactStore.removeFriendRequest(item.id); toast.success('已拒绝') } catch { toast.error('操作失败') } }
|
||||||
onMounted(async () => {
|
|
||||||
await loadRequests()
|
|
||||||
})
|
|
||||||
|
|
||||||
async function loadRequests() {
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
requests.value = await contactApi.getFriendRequests()
|
|
||||||
contactStore.setFriendRequests(requests.value)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载好友申请失败:', error)
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function accept(item: FriendRequest) {
|
|
||||||
try {
|
|
||||||
await contactApi.acceptFriendRequest(item.id)
|
|
||||||
item.status = 'accepted'
|
|
||||||
contactStore.removeFriendRequest(item.id)
|
|
||||||
toast.success('已添加')
|
|
||||||
} catch {
|
|
||||||
toast.error('操作失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function reject(item: FriendRequest) {
|
|
||||||
try {
|
|
||||||
await contactApi.rejectFriendRequest(item.id)
|
|
||||||
item.status = 'rejected'
|
|
||||||
contactStore.removeFriendRequest(item.id)
|
|
||||||
toast.success('已拒绝')
|
|
||||||
} catch {
|
|
||||||
toast.error('操作失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.requests-page {
|
.requests-page {
|
||||||
|
--bg-page: #f5f7fa;
|
||||||
|
--bg-surface: #ffffff;
|
||||||
|
--text-primary: #1d1d1f;
|
||||||
|
--text-secondary: #86868b;
|
||||||
|
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: var(--bg-page);
|
background: var(--bg-page);
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-state {
|
.requests-page.dark {
|
||||||
|
--bg-page: #1c1c1e;
|
||||||
|
--bg-surface: #2c2c2e;
|
||||||
|
--text-primary: #f2f2f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-body { padding: 30rpx; }
|
||||||
|
|
||||||
|
.loading-state { padding: 100rpx; display: flex; flex-direction: column; align-items: center; gap: 20rpx; color: var(--text-secondary); }
|
||||||
|
|
||||||
|
.request-card {
|
||||||
|
background: var(--bg-surface);
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 30rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
gap: 24rpx;
|
||||||
justify-content: center;
|
|
||||||
padding: 100rpx;
|
|
||||||
color: var(--text-tertiary);
|
|
||||||
gap: 20rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.request-item {
|
.card-left {
|
||||||
display: flex;
|
display: flex; align-items: flex-start;
|
||||||
align-items: center;
|
|
||||||
padding: 24rpx 30rpx;
|
|
||||||
background: var(--bg-content);
|
|
||||||
border-bottom: 1rpx solid var(--divider-color);
|
|
||||||
|
|
||||||
.avatar-placeholder {
|
|
||||||
width: 96rpx;
|
|
||||||
height: 96rpx;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: 8rpx;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 36rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.request-info {
|
|
||||||
flex: 1;
|
|
||||||
margin-left: 24rpx;
|
|
||||||
min-width: 0;
|
|
||||||
|
|
||||||
.name {
|
|
||||||
display: block;
|
|
||||||
font-size: 32rpx;
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.message {
|
.card-content {
|
||||||
display: block;
|
flex: 1;
|
||||||
font-size: 26rpx;
|
.info-top {
|
||||||
color: var(--text-secondary);
|
display: flex; justify-content: space-between; align-items: center; margin-bottom: 12rpx;
|
||||||
margin-top: 8rpx;
|
.name { font-size: 32rpx; font-weight: 600; color: var(--text-primary); }
|
||||||
overflow: hidden;
|
.time { font-size: 24rpx; color: var(--text-secondary); }
|
||||||
text-overflow: ellipsis;
|
}
|
||||||
white-space: nowrap;
|
.msg-box {
|
||||||
}
|
background: rgba(0,0,0,0.03); padding: 16rpx; border-radius: 12rpx;
|
||||||
}
|
.dark & { background: rgba(255,255,255,0.05); }
|
||||||
|
.message { font-size: 28rpx; color: var(--text-secondary); line-height: 1.4; }
|
||||||
.request-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 16rpx;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.status {
|
|
||||||
font-size: 26rpx;
|
|
||||||
color: var(--text-tertiary);
|
|
||||||
|
|
||||||
&.accepted {
|
|
||||||
color: var(--color-primary);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-footer {
|
||||||
|
display: flex; justify-content: flex-end; padding-top: 20rpx; border-top: 1rpx solid rgba(0,0,0,0.05);
|
||||||
|
.dark & { border-top-color: rgba(255,255,255,0.1); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端友好的按钮组 */
|
||||||
|
.btn-group {
|
||||||
|
display: flex; gap: 20rpx;
|
||||||
|
.action-btn {
|
||||||
|
padding: 12rpx 32rpx; border-radius: 30rpx; font-size: 26rpx; font-weight: 500;
|
||||||
|
&.reject { background: rgba(0,0,0,0.05); color: var(--text-primary); .dark & { background: rgba(255,255,255,0.1); } }
|
||||||
|
&.accept { background: #4f46e5; color: #fff; box-shadow: 0 4rpx 12rpx rgba(79, 70, 229, 0.3); }
|
||||||
|
&:active { opacity: 0.8; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
font-size: 26rpx; color: var(--text-secondary); font-weight: 500;
|
||||||
|
&.accepted { color: #4f46e5; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,20 +1,190 @@
|
|||||||
|
<template>
|
||||||
|
<wd-config-provider :theme="isDark ? 'dark' : 'light'">
|
||||||
|
<view class="page-container" :class="{ dark: isDark }">
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="nav-right">
|
||||||
|
<view class="icon-btn" @click="showPlusMenu = true">
|
||||||
|
<wd-icon name="add" size="52rpx" color="var(--text-primary)" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 3. 会话列表 -->
|
||||||
|
<scroll-view
|
||||||
|
class="conversation-list"
|
||||||
|
scroll-y
|
||||||
|
:refresher-enabled="true"
|
||||||
|
:refresher-triggered="refreshing"
|
||||||
|
@refresherrefresh="onRefresh"
|
||||||
|
>
|
||||||
|
<view v-if="loading && conversations.length === 0" class="loading-state">
|
||||||
|
<wd-loading size="50rpx" color="var(--color-primary)" />
|
||||||
|
<text>连接中...</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<wd-status-tip
|
||||||
|
v-else-if="conversations.length === 0"
|
||||||
|
image="message"
|
||||||
|
tip="暂无消息,开始聊天吧"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<wd-swipe-action
|
||||||
|
v-for="item in conversations"
|
||||||
|
:key="item.id"
|
||||||
|
:disabled="false"
|
||||||
|
>
|
||||||
|
<view
|
||||||
|
class="conversation-item"
|
||||||
|
:class="{ 'is-top': item.is_top }"
|
||||||
|
@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"
|
||||||
|
/>
|
||||||
|
<view v-if="item.is_special_care" class="special-badge-wrap">
|
||||||
|
<wd-icon name="heart-fill" size="20rpx" color="#fff" />
|
||||||
|
</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="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" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<template #right>
|
||||||
|
<view class="swipe-actions">
|
||||||
|
<view class="action-btn top" @click.stop="toggleTop(item)">
|
||||||
|
<wd-icon name="pin" size="40rpx" />
|
||||||
|
</view>
|
||||||
|
<view class="action-btn delete" @click.stop="deleteConversation(item)">
|
||||||
|
<wd-icon name="delete" size="40rpx" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
</wd-swipe-action>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<view class="safe-area-spacer"></view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<!-- 弹窗组件 -->
|
||||||
|
<wd-action-sheet
|
||||||
|
v-model="showPlusMenu"
|
||||||
|
:actions="plusMenuActions"
|
||||||
|
@select="onPlusMenuSelect"
|
||||||
|
:z-index="9999"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<wd-action-sheet
|
||||||
|
v-model="showConvActions"
|
||||||
|
:actions="convActionItems"
|
||||||
|
@select="onConvActionSelect"
|
||||||
|
cancel-text="取消"
|
||||||
|
:z-index="9999"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 侧边抽屉 -->
|
||||||
|
<wd-popup
|
||||||
|
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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<wd-toast />
|
||||||
|
<wd-message-box :z-index="11000" />
|
||||||
|
<app-tab-bar current="messages" />
|
||||||
|
</view>
|
||||||
|
</wd-config-provider>
|
||||||
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Conversation } from '@/types/conversation'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { onShow } from '@dcloudio/uni-app'
|
import { onShow } from '@dcloudio/uni-app'
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { useAuthStore, useConversationStore } from '@/stores'
|
||||||
import { useMessage, useToast } from 'wot-design-uni'
|
import { useTheme } from '@/composables/useTheme'
|
||||||
|
import { formatMessageTime } from '@/utils/format'
|
||||||
|
import { useToast, useMessage } from 'wot-design-uni'
|
||||||
import * as conversationApi from '@/api/modules/conversation'
|
import * as conversationApi from '@/api/modules/conversation'
|
||||||
import AppAvatar from '@/components/common/AppAvatar.vue'
|
import AppAvatar from '@/components/common/AppAvatar.vue'
|
||||||
import AppTabBar from '@/components/common/AppTabBar.vue'
|
import AppTabBar from '@/components/common/AppTabBar.vue'
|
||||||
import { useTheme } from '@/composables/useTheme'
|
import type { Conversation } from '@/types/conversation'
|
||||||
import { useAuthStore, useConversationStore } from '@/stores'
|
|
||||||
import { formatMessageTime } from '@/utils/format'
|
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const conversationStore = useConversationStore()
|
const conversationStore = useConversationStore()
|
||||||
const { isDark } = useTheme()
|
const { isDark } = useTheme()
|
||||||
|
|
||||||
// --- 保持原有逻辑代码不变 ---
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const messageBox = useMessage()
|
const messageBox = useMessage()
|
||||||
const searchKeyword = ref('')
|
const searchKeyword = ref('')
|
||||||
@@ -37,22 +207,19 @@ const themeText = computed(() => {
|
|||||||
const plusMenuActions = [
|
const plusMenuActions = [
|
||||||
{ name: '创建群聊', value: 'createGroup' },
|
{ name: '创建群聊', value: 'createGroup' },
|
||||||
{ name: '添加好友', value: 'addFriend' },
|
{ name: '添加好友', value: 'addFriend' },
|
||||||
{ name: '扫一扫', value: 'scan' },
|
{ name: '扫一扫', value: 'scan' }
|
||||||
]
|
]
|
||||||
|
|
||||||
const convActionItems = computed(() => {
|
const convActionItems = computed(() => {
|
||||||
if (!selectedConv.value)
|
if (!selectedConv.value) return []
|
||||||
return []
|
|
||||||
const conv = selectedConv.value
|
const conv = selectedConv.value
|
||||||
const isGroup = conv.is_group || conv.type === 2
|
const isGroup = conv.is_group || conv.type === 2
|
||||||
const items: any[] = [
|
const items: any[] = [
|
||||||
{ name: conv.is_top ? '取消置顶' : '置顶会话', value: 'toggleTop' },
|
{ name: conv.is_top ? '取消置顶' : '置顶会话', value: 'toggleTop' },
|
||||||
{ name: conv.is_muted ? '开启提醒' : '消息免打扰', value: 'toggleMute' },
|
{ name: conv.is_muted ? '开启提醒' : '消息免打扰', value: 'toggleMute' }
|
||||||
]
|
]
|
||||||
if (!isGroup)
|
if (!isGroup) items.push({ name: conv.is_special_care ? '取消特别关心' : '特别关心', value: 'toggleSpecial' })
|
||||||
items.push({ name: conv.is_special_care ? '取消特别关心' : '特别关心', value: 'toggleSpecial' })
|
if (conv.unread_count && conv.unread_count > 0) items.push({ name: '标记已读', value: 'markRead' })
|
||||||
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: '#fa5151' })
|
||||||
return items
|
return items
|
||||||
})
|
})
|
||||||
@@ -80,9 +247,12 @@ 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 || '')}` })
|
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 }
|
function handleLongPress(item: Conversation) { selectedConv.value = item; showConvActions.value = true }
|
||||||
async function onConvActionSelect(action: { value: string }) {
|
async function onConvActionSelect(event: any) {
|
||||||
if (!selectedConv.value)
|
// Wot-UI select 事件通常返回 { item, index },或者直接是 detail 对象
|
||||||
return
|
// 防御性获取 item
|
||||||
|
const action = event.item || event
|
||||||
|
|
||||||
|
if (!selectedConv.value) return
|
||||||
const conv = selectedConv.value
|
const conv = selectedConv.value
|
||||||
showConvActions.value = false
|
showConvActions.value = false
|
||||||
try {
|
try {
|
||||||
@@ -90,547 +260,180 @@ async function onConvActionSelect(action: { value: string }) {
|
|||||||
await conversationApi.updateConversation({ target_id: conv.target_id, is_top: !conv.is_top })
|
await conversationApi.updateConversation({ target_id: conv.target_id, is_top: !conv.is_top })
|
||||||
conv.is_top = !conv.is_top
|
conv.is_top = !conv.is_top
|
||||||
conversationStore.sortConversations()
|
conversationStore.sortConversations()
|
||||||
}
|
} else if (action.value === 'toggleMute') {
|
||||||
else if (action.value === 'toggleMute') {
|
|
||||||
await conversationApi.updateConversation({ target_id: conv.target_id, is_muted: !conv.is_muted })
|
await conversationApi.updateConversation({ target_id: conv.target_id, is_muted: !conv.is_muted })
|
||||||
conv.is_muted = !conv.is_muted
|
conv.is_muted = !conv.is_muted
|
||||||
}
|
} else if (action.value === 'toggleSpecial') {
|
||||||
else if (action.value === 'toggleSpecial') {
|
|
||||||
await conversationApi.updateConversation({ target_id: conv.target_id, is_special_care: !conv.is_special_care })
|
await conversationApi.updateConversation({ target_id: conv.target_id, is_special_care: !conv.is_special_care })
|
||||||
conv.is_special_care = !conv.is_special_care
|
conv.is_special_care = !conv.is_special_care
|
||||||
}
|
} else if (action.value === 'markRead') {
|
||||||
else if (action.value === 'markRead') {
|
|
||||||
await conversationStore.clearUnread(conv.target_id)
|
await conversationStore.clearUnread(conv.target_id)
|
||||||
}
|
} else if (action.value === 'delete') {
|
||||||
else if (action.value === 'delete') {
|
|
||||||
await deleteConversation(conv)
|
await deleteConversation(conv)
|
||||||
}
|
}
|
||||||
}
|
} catch(e) { toast.error('操作失败') }
|
||||||
catch (e) { toast.error('操作失败') }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提取删除逻辑
|
|
||||||
async function deleteConversation(item: Conversation) {
|
async function deleteConversation(item: Conversation) {
|
||||||
try {
|
try {
|
||||||
await messageBox.confirm({ title: '提示', msg: '确定删除该会话吗?' })
|
await messageBox.confirm({ title: '提示', msg: '确定删除该会话吗?' })
|
||||||
await conversationApi.deleteConversation(item.target_id)
|
await conversationApi.deleteConversation(item.target_id)
|
||||||
const index = conversationStore.conversations.findIndex(c => c.id === item.id)
|
const index = conversationStore.conversations.findIndex(c => c.id === item.id)
|
||||||
if (index > -1)
|
if (index > -1) conversationStore.conversations.splice(index, 1)
|
||||||
conversationStore.conversations.splice(index, 1)
|
|
||||||
toast.success('已删除')
|
toast.success('已删除')
|
||||||
}
|
} catch (e: any) { if(e !== 'cancel') toast.error('删除失败') }
|
||||||
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') {
|
||||||
|
uni.navigateTo({ url: '/pages/group/create' })
|
||||||
|
} else if (item && item.value === 'addFriend') {
|
||||||
|
uni.navigateTo({ url: '/pages/contact/add' })
|
||||||
|
} else if (item && item.value === 'scan') {
|
||||||
|
uni.scanCode({ success: () => toast.show('扫码成功'), fail: () => toast.error('扫码失败') })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPlusMenuSelect(item: { value: string }) {
|
|
||||||
showPlusMenu.value = false
|
|
||||||
if (item.value === 'createGroup')
|
|
||||||
uni.navigateTo({ url: '/pages/group/create' })
|
|
||||||
else if (item.value === 'addFriend')
|
|
||||||
uni.navigateTo({ url: '/pages/contact/add' })
|
|
||||||
else if (item.value === 'scan')
|
|
||||||
uni.scanCode({ success: () => toast.show('扫码成功'), fail: () => toast.error('扫码失败') })
|
|
||||||
}
|
|
||||||
function openDrawer() { showDrawer.value = true }
|
function openDrawer() { showDrawer.value = true }
|
||||||
function goProfile() { showDrawer.value = false; uni.navigateTo({ url: '/pages/profile/index' }) }
|
function goProfile() { showDrawer.value = false; uni.navigateTo({ url: '/pages/profile/index' }) }
|
||||||
function goSettings() { showDrawer.value = false; uni.navigateTo({ url: '/pages/settings/index' }) }
|
function goSettings() { showDrawer.value = false; uni.navigateTo({ url: '/pages/settings/index' }) }
|
||||||
function goTheme() { showDrawer.value = false; uni.navigateTo({ url: '/pages/settings/theme' }) }
|
function goTheme() { showDrawer.value = false; uni.navigateTo({ url: '/pages/settings/theme' }) }
|
||||||
async function logout() {
|
async function logout() {
|
||||||
try { await messageBox.confirm({ title: '提示', msg: '确定退出?' }); showDrawer.value = false; authStore.logout() }
|
try { await messageBox.confirm({ title: '提示', msg: '确定退出?' }); showDrawer.value = false; authStore.logout() } catch {}
|
||||||
catch {}
|
|
||||||
}
|
}
|
||||||
// --- 保持逻辑代码结束 ---
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
|
||||||
<!--
|
|
||||||
WotUI 最佳实践:
|
|
||||||
使用 wd-config-provider 包裹页面以确保组件级暗黑模式生效
|
|
||||||
custom-style 用于注入我们定义的 CSS 变量
|
|
||||||
-->
|
|
||||||
<wd-config-provider :theme="isDark ? 'dark' : 'light'">
|
|
||||||
<view class="page-container" :class="{ dark: isDark }">
|
|
||||||
<!-- 沉浸式导航栏 -->
|
|
||||||
<view class="custom-navbar">
|
|
||||||
<view class="navbar-bg" /> <!-- 独立背景层用于做高斯模糊 -->
|
|
||||||
<view class="navbar-content">
|
|
||||||
<view class="navbar-left" @click="openDrawer">
|
|
||||||
<app-avatar
|
|
||||||
:src="user?.avatar"
|
|
||||||
:name="user?.name"
|
|
||||||
:size="72"
|
|
||||||
round
|
|
||||||
custom-style="border: 2rpx solid var(--border-subtle);"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
<view class="navbar-title">
|
|
||||||
<text class="title-text">
|
|
||||||
{{ user?.name || '消息' }}
|
|
||||||
</text>
|
|
||||||
<view v-if="!loading" class="online-status" />
|
|
||||||
</view>
|
|
||||||
<view class="navbar-right">
|
|
||||||
<view class="icon-btn" @click="showPlusMenu = true">
|
|
||||||
<wd-icon name="add" size="44rpx" />
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 搜索区域 (模拟 iOS 风格) -->
|
|
||||||
<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>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 会话列表 -->
|
|
||||||
<scroll-view
|
|
||||||
class="conversation-list"
|
|
||||||
scroll-y
|
|
||||||
:refresher-enabled="true"
|
|
||||||
:refresher-triggered="refreshing"
|
|
||||||
@refresherrefresh="onRefresh"
|
|
||||||
>
|
|
||||||
<!-- 骨架屏/加载态 (优化体验) -->
|
|
||||||
<view v-if="loading && conversations.length === 0" class="loading-state">
|
|
||||||
<wd-loading size="60rpx" color="var(--color-primary)" />
|
|
||||||
<text>同步消息中...</text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<wd-status-tip
|
|
||||||
v-else-if="conversations.length === 0"
|
|
||||||
image="message"
|
|
||||||
tip="暂无消息,开始聊天吧"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<template v-else>
|
|
||||||
<!-- 列表渲染 -->
|
|
||||||
<wd-swipe-action
|
|
||||||
v-for="item in conversations"
|
|
||||||
:key="item.id"
|
|
||||||
:disabled="false"
|
|
||||||
>
|
|
||||||
<view
|
|
||||||
class="conversation-item"
|
|
||||||
:class="{ 'is-top': item.is_top }"
|
|
||||||
@click="goChat(item)"
|
|
||||||
@longpress="handleLongPress(item)"
|
|
||||||
>
|
|
||||||
<!-- 头像区域 -->
|
|
||||||
<view class="avatar-wrap">
|
|
||||||
<app-avatar
|
|
||||||
:src="item.avatar"
|
|
||||||
:name="item.name"
|
|
||||||
:size="104"
|
|
||||||
radius="16rpx"
|
|
||||||
:badge="item.is_muted ? 0 : item.unread_count"
|
|
||||||
:dot="item.is_muted && item.unread_count > 0"
|
|
||||||
/>
|
|
||||||
<!-- 特别关心采用更现代的角标 -->
|
|
||||||
<view v-if="item.is_special_care" class="special-badge-wrap">
|
|
||||||
<wd-icon name="heart-fill" size="20rpx" color="#fff" />
|
|
||||||
</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="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" />
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 右滑菜单 -->
|
|
||||||
<template #right>
|
|
||||||
<view class="swipe-actions">
|
|
||||||
<view class="action-btn top" @click.stop="toggleTop(item)">
|
|
||||||
<wd-icon name="pin" size="36rpx" />
|
|
||||||
</view>
|
|
||||||
<view class="action-btn delete" @click.stop="deleteConversation(item)">
|
|
||||||
<wd-icon name="delete" size="36rpx" />
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
</wd-swipe-action>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- 底部垫片 -->
|
|
||||||
<view class="safe-area-spacer" />
|
|
||||||
</scroll-view>
|
|
||||||
|
|
||||||
<!-- 弹窗组件保持原有逻辑 -->
|
|
||||||
<wd-action-sheet
|
|
||||||
v-model="showPlusMenu"
|
|
||||||
:actions="plusMenuActions"
|
|
||||||
@select="onPlusMenuSelect"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<wd-action-sheet
|
|
||||||
v-model="showConvActions"
|
|
||||||
:actions="convActionItems"
|
|
||||||
cancel-text="取消"
|
|
||||||
@select="onConvActionSelect"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 侧边抽屉 -->
|
|
||||||
<wd-popup
|
|
||||||
v-model="showDrawer"
|
|
||||||
position="left"
|
|
||||||
custom-style="width: 75%; height: 100%; background: var(--bg-surface);"
|
|
||||||
>
|
|
||||||
<view class="drawer-container">
|
|
||||||
<view class="drawer-header-bg" />
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<wd-toast />
|
|
||||||
<wd-message-box />
|
|
||||||
<app-tab-bar current="messages" />
|
|
||||||
</view>
|
|
||||||
</wd-config-provider>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
/* ----- 语义化 CSS 变量定义 ----- */
|
|
||||||
.page-container {
|
.page-container {
|
||||||
/* 亮色模式默认值 */
|
|
||||||
--bg-page: #f7f8fa;
|
--bg-page: #f7f8fa;
|
||||||
--bg-surface: #ffffff;
|
--bg-surface: #ffffff;
|
||||||
--bg-hover: #f5f5f5;
|
--bg-hover: #f5f5f5;
|
||||||
--text-primary: #1d1d1f;
|
--text-primary: #1d1d1f;
|
||||||
--text-secondary: #86868b;
|
--text-secondary: #86868b;
|
||||||
--text-tertiary: #a1a1a6;
|
--text-tertiary: #a1a1a6;
|
||||||
--border-subtle: #ebedf0;
|
--border-subtle: rgba(0,0,0,0.05);
|
||||||
--nav-bg: rgba(255, 255, 255, 0.85);
|
--nav-bg: rgba(255, 255, 255, 0.85);
|
||||||
--color-brand: #4f46e5;
|
--color-brand: #4f46e5;
|
||||||
--color-danger: #ef4444;
|
|
||||||
--shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
|
|
||||||
|
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: var(--bg-page);
|
background: var(--bg-page);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
transition: background-color 0.3s, color 0.3s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 暗色模式覆盖 */
|
|
||||||
.page-container.dark {
|
.page-container.dark {
|
||||||
--bg-page: #000000;
|
--bg-page: #1c1c1e;
|
||||||
--bg-surface: #1c1c1e;
|
--bg-surface: #2c2c2e;
|
||||||
--bg-hover: #2c2c2e;
|
--bg-hover: #3a3a3c;
|
||||||
--text-primary: #ffffff;
|
--text-primary: #f2f2f7;
|
||||||
--text-secondary: #98989d;
|
--text-secondary: #aeaeb2;
|
||||||
--text-tertiary: #636366;
|
--text-tertiary: #636366;
|
||||||
--border-subtle: #38383a;
|
--border-subtle: rgba(255,255,255,0.1);
|
||||||
--nav-bg: rgba(28, 28, 30, 0.85);
|
--nav-bg: rgba(28, 28, 30, 0.85);
|
||||||
--shadow-sm: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ----- UI 样式重构 ----- */
|
|
||||||
|
|
||||||
/* 1. 沉浸式导航栏 */
|
|
||||||
.custom-navbar {
|
.custom-navbar {
|
||||||
position: sticky;
|
position: sticky; top: 0; z-index: 100;
|
||||||
top: 0;
|
|
||||||
z-index: 0;
|
|
||||||
padding-top: var(--status-bar-height);
|
padding-top: var(--status-bar-height);
|
||||||
|
|
||||||
.navbar-bg {
|
.navbar-bg {
|
||||||
position: absolute;
|
position: absolute; inset: 0;
|
||||||
inset: 0;
|
background: var(--nav-bg); backdrop-filter: blur(20px);
|
||||||
background: var(--nav-bg);
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
-webkit-backdrop-filter: blur(20px);
|
|
||||||
border-bottom: 1px solid var(--border-subtle);
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-content {
|
.navbar-content {
|
||||||
position: relative;
|
position: relative; height: 44px;
|
||||||
height: 44px; /* iOS 标准导航高度 */
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0 32rpx;
|
padding: 0 32rpx;
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-title {
|
.nav-left-group {
|
||||||
position: absolute;
|
display: flex; align-items: center;
|
||||||
left: 50%;
|
&:active { opacity: 0.7; }
|
||||||
transform: translateX(-50%);
|
|
||||||
font-size: 34rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8rpx;
|
|
||||||
|
|
||||||
.online-status {
|
.nav-info {
|
||||||
width: 12rpx;
|
display: flex; flex-direction: column; justify-content: center;
|
||||||
height: 12rpx;
|
margin-left: 16rpx;
|
||||||
background: #34c759;
|
.nav-title { font-size: 34rpx; font-weight: 700; color: var(--text-primary); line-height: 1.2; }
|
||||||
border-radius: 50%;
|
.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; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-btn {
|
.icon-btn {
|
||||||
width: 64rpx;
|
width: 72rpx; height: 72rpx; display: flex; align-items: center; justify-content: center; border-radius: 50%;
|
||||||
height: 64rpx;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: 50%;
|
|
||||||
&:active { background: var(--bg-hover); }
|
&:active { background: var(--bg-hover); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 2. 搜索框 (仿原生) */
|
|
||||||
.search-wrapper {
|
.search-wrapper {
|
||||||
background: var(--bg-page);
|
background: var(--bg-page); padding: 16rpx 32rpx;
|
||||||
padding: 16rpx 32rpx;
|
position: sticky; top: calc(44px + var(--status-bar-height)); z-index: 99;
|
||||||
position: sticky;
|
|
||||||
top: calc(44px + var(--status-bar-height));
|
|
||||||
z-index: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-inner {
|
.search-inner {
|
||||||
height: 72rpx;
|
height: 72rpx; background: var(--bg-surface); border-radius: 16rpx;
|
||||||
background: var(--bg-surface);
|
display: flex; align-items: center; justify-content: center; gap: 12rpx;
|
||||||
border-radius: 16rpx;
|
.search-placeholder { font-size: 28rpx; color: var(--text-tertiary); }
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 12rpx;
|
|
||||||
|
|
||||||
.search-placeholder {
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: var(--text-tertiary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 3. 列表区域 */
|
|
||||||
.conversation-list {
|
|
||||||
/* 移除复杂的 calc,利用 Flex 布局或 padding */
|
|
||||||
height: calc(100vh - 44px - var(--status-bar-height) - 104rpx); /* 减去 nav 和 search */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.conversation-list { height: calc(100vh - 44px - var(--status-bar-height) - 104rpx); }
|
||||||
.conversation-item {
|
.conversation-item {
|
||||||
display: flex;
|
display: flex; padding: 24rpx 32rpx; background: var(--bg-surface);
|
||||||
padding: 24rpx 32rpx;
|
transition: background-color 0.2s;
|
||||||
background: var(--bg-surface);
|
&:active { background: var(--bg-hover); }
|
||||||
transition: all 0.2s;
|
&.is-top { background: var(--bg-page); }
|
||||||
|
.content-wrap { border-bottom: 1rpx solid var(--border-subtle); }
|
||||||
&:active {
|
&:last-child .content-wrap { border-bottom: none; }
|
||||||
background: var(--bg-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-top {
|
|
||||||
background: var(--bg-page); /* 置顶颜色微调 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 边框处理:只给内容区加底边框,头像不加,模仿 iOS */
|
|
||||||
.content-wrap {
|
|
||||||
border-bottom: 1px solid var(--border-subtle);
|
|
||||||
}
|
|
||||||
&:last-child .content-wrap {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar-wrap {
|
.avatar-wrap {
|
||||||
position: relative;
|
position: relative; margin-right: 24rpx; flex-shrink: 0;
|
||||||
margin-right: 24rpx;
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
.special-badge-wrap {
|
.special-badge-wrap {
|
||||||
position: absolute;
|
position: absolute; bottom: -6rpx; right: -6rpx;
|
||||||
bottom: -6rpx;
|
background: #ec4899; border: 4rpx solid var(--bg-surface); border-radius: 50%;
|
||||||
right: -6rpx;
|
width: 36rpx; height: 36rpx; display: flex; align-items: center; justify-content: center;
|
||||||
background: #ec4899;
|
|
||||||
border: 4rpx solid var(--bg-surface);
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 36rpx;
|
|
||||||
height: 36rpx;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-wrap {
|
.content-wrap { flex: 1; min-width: 0; display: flex; flex-direction: column; justify-content: center; padding-right: 4rpx; }
|
||||||
flex: 1;
|
.row-top, .row-bottom { display: flex; align-items: center; justify-content: space-between; }
|
||||||
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; }
|
.row-top { margin-bottom: 8rpx; }
|
||||||
|
|
||||||
.title-box {
|
.title-box {
|
||||||
display: flex;
|
display: flex; align-items: center; flex: 1;
|
||||||
align-items: center;
|
.pin-icon { color: var(--color-brand); margin-right: 8rpx; transform: rotate(45deg); }
|
||||||
flex: 1;
|
.name { font-size: 32rpx; font-weight: 600; color: var(--text-primary); &.special-text { color: #ec4899; } }
|
||||||
|
|
||||||
.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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 4. 滑动菜单 */
|
|
||||||
.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 {
|
.action-btn {
|
||||||
width: 140rpx;
|
width: 140rpx; height: 100%; display: flex; align-items: center; justify-content: center; color: #fff;
|
||||||
height: 100%;
|
&.top { background: #4f46e5; } &.delete { background: #ef4444; }
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: #fff;
|
|
||||||
|
|
||||||
&.top { background: #4f46e5; }
|
|
||||||
&.delete { background: #ef4444; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 5. 抽屉重构 */
|
|
||||||
.drawer-container {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
position: relative;
|
|
||||||
z-index: 50000;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.drawer-container { height: 100%; display: flex; flex-direction: column; position: relative; overflow: hidden; background: var(--bg-surface); }
|
||||||
.drawer-header-bg {
|
.drawer-header-bg {
|
||||||
position: absolute;
|
position: absolute; top: 0; left: 0; width: 100%; height: 300rpx;
|
||||||
top: 0;
|
background: linear-gradient(135deg, var(--color-brand), rgba(79, 70, 229, 0.2)); opacity: 0.1; z-index: 0;
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 300rpx;
|
|
||||||
background: linear-gradient(135deg, var(--color-brand), #818cf8);
|
|
||||||
opacity: 0.1;
|
|
||||||
z-index: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.drawer-info {
|
.drawer-info {
|
||||||
position: relative;
|
position: relative; z-index: 1; padding: 100rpx 40rpx 60rpx; display: flex; flex-direction: column;
|
||||||
z-index: 1000;
|
.drawer-username { font-size: 40rpx; font-weight: 700; margin-top: 24rpx; color: var(--text-primary); }
|
||||||
padding: 100rpx 40rpx 60rpx;
|
.drawer-bio { font-size: 26rpx; color: var(--text-secondary); margin-top: 12rpx; }
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.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; /* 留出 Tabbar 空间 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-state {
|
|
||||||
padding-top: 200rpx;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
color: var(--text-tertiary);
|
|
||||||
gap: 20rpx;
|
|
||||||
font-size: 26rpx;
|
|
||||||
}
|
}
|
||||||
|
.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>
|
</style>
|
||||||
|
|||||||
@@ -1,169 +1,162 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="page-container" :class="{ dark: isDark }">
|
<wd-config-provider :theme="isDark ? 'dark' : 'light'">
|
||||||
<!-- 自定义导航栏 -->
|
<view class="moments-page" :class="{ dark: isDark }">
|
||||||
<view class="custom-navbar">
|
<!-- 1. 沉浸式渐变导航栏 -->
|
||||||
<view class="navbar-content">
|
<!-- 使用动态背景色实现滚动渐变 -->
|
||||||
<view class="navbar-title">朋友圈</view>
|
<view class="nav-header" :style="{ background: `rgba(var(--bg-nav-rgb), ${navOpacity})`, borderBottom: navOpacity > 0.8 ? '1rpx solid var(--border-color)' : 'none' }">
|
||||||
<view class="navbar-right">
|
<view class="nav-content">
|
||||||
<wd-badge :value="unreadCount" :max="99" v-if="unreadCount > 0">
|
<text class="nav-title" :style="{ opacity: navOpacity }">朋友圈</text>
|
||||||
<wd-icon name="bell" size="44rpx" @click="goNotifications" />
|
|
||||||
</wd-badge>
|
|
||||||
<wd-icon v-else name="bell" size="44rpx" @click="goNotifications" />
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 内容区域 -->
|
<!-- 保留原有的消息通知逻辑:铃铛图标 + 红点 -->
|
||||||
<scroll-view
|
<view class="nav-right">
|
||||||
class="content-scroll"
|
<wd-badge :value="unreadCount" :max="99" v-if="unreadCount > 0">
|
||||||
scroll-y
|
<!-- 根据背景透明度自动切换图标颜色 -->
|
||||||
:refresher-enabled="true"
|
<wd-icon name="bell" size="44rpx" :color="iconColor" @click="goNotifications" />
|
||||||
:refresher-triggered="refreshing"
|
</wd-badge>
|
||||||
@refresherrefresh="onRefresh"
|
<wd-icon v-else name="bell" size="44rpx" :color="iconColor" @click="goNotifications" />
|
||||||
@scrolltolower="loadMore"
|
|
||||||
>
|
|
||||||
<!-- 封面区域 -->
|
|
||||||
<view class="cover-section">
|
|
||||||
<image class="cover-bg" src="/static/images/moment-cover.jpg" mode="aspectFill" />
|
|
||||||
<view class="user-info">
|
|
||||||
<wd-img
|
|
||||||
v-if="user?.avatar"
|
|
||||||
:src="resolveImageUrl(user.avatar)"
|
|
||||||
width="120rpx"
|
|
||||||
height="120rpx"
|
|
||||||
radius="8rpx"
|
|
||||||
/>
|
|
||||||
<view v-else class="avatar-placeholder avatar-lg">
|
|
||||||
{{ user?.name?.charAt(0) || '?' }}
|
|
||||||
</view>
|
</view>
|
||||||
<text class="user-name">{{ user?.name || '未知' }}</text>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 动态列表 -->
|
<!-- 2. 内容滚动区 -->
|
||||||
<view class="moments-list">
|
<scroll-view
|
||||||
<view v-if="loading && moments.length === 0" class="loading-state">
|
scroll-y
|
||||||
<wd-loading />
|
class="scroll-container"
|
||||||
<text>加载中...</text>
|
:refresher-enabled="true"
|
||||||
|
:refresher-triggered="refreshing"
|
||||||
|
@scroll="onPageScroll"
|
||||||
|
@refresherrefresh="onRefresh"
|
||||||
|
@scrolltolower="loadMore"
|
||||||
|
>
|
||||||
|
<!-- 封面图区域 -->
|
||||||
|
<view class="cover-area">
|
||||||
|
<image src="/static/images/moment-cover.jpg" 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" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<wd-status-tip v-else-if="moments.length === 0" tip="暂无动态" />
|
<!-- 列表内容 -->
|
||||||
|
<view class="feed-list">
|
||||||
|
<view v-if="loading && moments.length === 0" class="loading-state">
|
||||||
|
<wd-loading />
|
||||||
|
<text>加载中...</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
<template v-else>
|
<wd-status-tip v-else-if="moments.length === 0" tip="暂无动态" />
|
||||||
<view
|
|
||||||
v-for="moment in moments"
|
|
||||||
:key="moment.id"
|
|
||||||
class="moment-card"
|
|
||||||
>
|
|
||||||
<!-- 用户头像 -->
|
|
||||||
<view class="moment-avatar">
|
|
||||||
<wd-img
|
|
||||||
v-if="moment.user?.avatar"
|
|
||||||
:src="resolveImageUrl(moment.user.avatar)"
|
|
||||||
width="80rpx"
|
|
||||||
height="80rpx"
|
|
||||||
radius="8rpx"
|
|
||||||
/>
|
|
||||||
<view v-else class="avatar-placeholder" :style="{ background: generateColor(moment.user?.name || '') }">
|
|
||||||
{{ moment.user?.name?.charAt(0) || '?' }}
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 动态内容 -->
|
<template v-else>
|
||||||
<view class="moment-content">
|
<!-- 循环变量名恢复为 moment,匹配原有逻辑 -->
|
||||||
<!-- 用户名 -->
|
<view v-for="moment in moments" :key="moment.id" class="feed-item">
|
||||||
<text class="moment-username">{{ moment.user?.name || '未知' }}</text>
|
<!-- 头像 (移除原代码中不存在的点击事件) -->
|
||||||
|
<view class="feed-avatar">
|
||||||
<!-- 文本内容 -->
|
<app-avatar :src="moment.user?.avatar" :name="moment.user?.name" :size="88" radius="12rpx" />
|
||||||
<view v-if="moment.content" class="moment-text">
|
|
||||||
<text>{{ moment.content }}</text>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 图片 -->
|
<view class="feed-content">
|
||||||
<view v-if="moment.media_type === 1" class="moment-images" :class="`images-${getImageCount(moment)}`">
|
<text class="nickname">{{ moment.user?.name || '未知' }}</text>
|
||||||
<wd-img
|
|
||||||
v-for="(url, index) in parseMediaUrls(moment.media_urls)"
|
|
||||||
:key="index"
|
|
||||||
:src="resolveImageUrl(url)"
|
|
||||||
:width="getImageSize(moment)"
|
|
||||||
:height="getImageSize(moment)"
|
|
||||||
mode="aspectFill"
|
|
||||||
radius="8rpx"
|
|
||||||
enable-preview
|
|
||||||
:preview-src-list="parseMediaUrls(moment.media_urls).map(resolveImageUrl)"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 视频 -->
|
<text v-if="moment.content" class="text-body">{{ moment.content }}</text>
|
||||||
<view v-if="moment.media_type === 2" class="moment-video">
|
|
||||||
<video
|
|
||||||
:src="resolveImageUrl(parseMediaUrls(moment.media_urls)[0])"
|
|
||||||
class="video-player"
|
|
||||||
object-fit="cover"
|
|
||||||
:show-fullscreen-btn="true"
|
|
||||||
:show-play-btn="true"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 时间和操作 -->
|
<!-- 图片九宫格 -->
|
||||||
<view class="moment-footer">
|
<view v-if="moment.media_type === 1" class="media-grid" :class="`grid-${getImageCount(moment)}`">
|
||||||
<text class="moment-time">{{ formatTime(moment.created_at) }}</text>
|
<view
|
||||||
<view class="moment-actions">
|
v-for="(url, index) in parseMediaUrls(moment.media_urls)"
|
||||||
<view class="action-item" @click="showActionMenu(moment)">
|
:key="index"
|
||||||
<wd-icon name="more" size="32rpx" />
|
class="grid-img-wrap"
|
||||||
|
:style="{ width: getImageSize(moment), height: getImageSize(moment) }"
|
||||||
|
>
|
||||||
|
<wd-img
|
||||||
|
:src="resolveImageUrl(url)"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
mode="aspectFill"
|
||||||
|
radius="8rpx"
|
||||||
|
enable-preview
|
||||||
|
:preview-src-list="parseMediaUrls(moment.media_urls).map(resolveImageUrl)"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 视频 -->
|
||||||
|
<view v-if="moment.media_type === 2" class="video-wrap">
|
||||||
|
<video
|
||||||
|
:src="resolveImageUrl(parseMediaUrls(moment.media_urls)[0])"
|
||||||
|
class="video-player"
|
||||||
|
object-fit="cover"
|
||||||
|
:show-fullscreen-btn="true"
|
||||||
|
:show-play-btn="true"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 底部操作栏 -->
|
||||||
|
<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)" />
|
||||||
|
</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)" />
|
||||||
|
<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"
|
||||||
|
:key="comment.id"
|
||||||
|
class="comment-row"
|
||||||
|
@click="goComment"
|
||||||
|
>
|
||||||
|
<text class="user-link">{{ comment.user?.name || '未知' }}</text>
|
||||||
|
<text v-if="comment.reply_to_user"> 回复 <text class="user-link">{{ comment.reply_to_user.name }}</text></text>
|
||||||
|
<text class="content">: {{ comment.content }}</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 点赞和评论 -->
|
|
||||||
<view v-if="(moment.likes && moment.likes.length > 0) || (moment.comments && moment.comments.length > 0)" class="moment-interactions">
|
|
||||||
<!-- 点赞 -->
|
|
||||||
<view v-if="moment.likes && moment.likes.length > 0" class="likes-row">
|
|
||||||
<wd-icon name="heart-fill" size="28rpx" color="#fa5151" />
|
|
||||||
<text class="likes-text">
|
|
||||||
{{ moment.likes.map(l => l.user?.name || '').join('、') }}
|
|
||||||
</text>
|
|
||||||
</view>
|
|
||||||
<!-- 评论 -->
|
|
||||||
<view
|
|
||||||
v-for="comment in moment.comments"
|
|
||||||
:key="comment.id"
|
|
||||||
class="comment-row"
|
|
||||||
>
|
|
||||||
<text class="comment-user">{{ comment.user?.name || '未知' }}</text>
|
|
||||||
<text v-if="comment.reply_to_user">
|
|
||||||
回复 <text class="comment-user">{{ comment.reply_to_user.name }}</text>
|
|
||||||
</text>
|
|
||||||
<text>: {{ comment.content }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 加载更多 -->
|
<!-- 加载更多 -->
|
||||||
<wd-loadmore :state="loadState" @loadmore="loadMore" />
|
<wd-loadmore :state="loadState" @loadmore="loadMore" />
|
||||||
</template>
|
</template>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<!-- 悬浮发布按钮 -->
|
||||||
|
<view class="fab-btn" @click="goPublish">
|
||||||
|
<wd-icon name="camera" size="52rpx" color="#fff" />
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
|
||||||
|
|
||||||
<!-- 发布按钮 -->
|
<!-- 操作菜单 -->
|
||||||
<view class="fab" @click="goPublish">
|
<wd-action-sheet
|
||||||
<wd-icon name="camera" size="48rpx" color="#fff" />
|
v-model="showActions"
|
||||||
|
:actions="actionItems"
|
||||||
|
@select="onActionSelect"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<wd-toast />
|
||||||
|
<wd-message-box />
|
||||||
|
|
||||||
|
<!-- TabBar (保留) -->
|
||||||
|
<app-tab-bar current="moments" />
|
||||||
</view>
|
</view>
|
||||||
|
</wd-config-provider>
|
||||||
<!-- 操作菜单 -->
|
|
||||||
<wd-action-sheet
|
|
||||||
v-model="showActions"
|
|
||||||
:actions="actionItems"
|
|
||||||
@select="onActionSelect"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<wd-toast />
|
|
||||||
<wd-message-box />
|
|
||||||
|
|
||||||
<!-- 自定义 TabBar -->
|
|
||||||
<app-tab-bar current="moments" />
|
|
||||||
</view>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -184,19 +177,27 @@ const { isDark } = useTheme()
|
|||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const messageBox = useMessage()
|
const messageBox = useMessage()
|
||||||
|
|
||||||
// 状态
|
// --- UI 状态 (新增) ---
|
||||||
|
const navOpacity = ref(0)
|
||||||
|
const iconColor = computed(() => {
|
||||||
|
// 当导航栏变白时,图标变黑;否则为白色
|
||||||
|
if (navOpacity.value > 0.6) return 'var(--text-main)'
|
||||||
|
return '#ffffff'
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- 原有状态保留 ---
|
||||||
const refreshing = ref(false)
|
const refreshing = ref(false)
|
||||||
const showActions = ref(false)
|
const showActions = ref(false)
|
||||||
const currentMoment = ref<Moment | null>(null)
|
const currentMoment = ref<Moment | null>(null)
|
||||||
const loadState = ref<'loading' | 'finished' | 'error'>('loading')
|
const loadState = ref<'loading' | 'finished' | 'error'>('loading')
|
||||||
|
|
||||||
// 计算属性
|
// --- 原有计算属性保留 ---
|
||||||
const user = computed(() => authStore.user)
|
const user = computed(() => authStore.user)
|
||||||
const moments = computed(() => momentStore.moments)
|
const moments = computed(() => momentStore.moments)
|
||||||
const loading = computed(() => momentStore.loading)
|
const loading = computed(() => momentStore.loading)
|
||||||
const unreadCount = computed(() => momentStore.unreadCount)
|
const unreadCount = computed(() => momentStore.unreadCount)
|
||||||
|
|
||||||
// 操作菜单
|
// --- 原有菜单逻辑保留 ---
|
||||||
const actionItems = computed(() => {
|
const actionItems = computed(() => {
|
||||||
const items = []
|
const items = []
|
||||||
if (currentMoment.value) {
|
if (currentMoment.value) {
|
||||||
@@ -212,7 +213,7 @@ const actionItems = computed(() => {
|
|||||||
return items
|
return items
|
||||||
})
|
})
|
||||||
|
|
||||||
// 生命周期
|
// --- 生命周期 ---
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadData()
|
loadData()
|
||||||
})
|
})
|
||||||
@@ -221,7 +222,14 @@ onShow(() => {
|
|||||||
loadUnreadCount()
|
loadUnreadCount()
|
||||||
})
|
})
|
||||||
|
|
||||||
// 方法
|
// --- 新增滚动监听 (UI逻辑) ---
|
||||||
|
function onPageScroll(e: any) {
|
||||||
|
const scrollTop = e.detail.scrollTop
|
||||||
|
// 滚动 200px 后导航栏完全不透明
|
||||||
|
navOpacity.value = Math.min(scrollTop / 200, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 原有方法保留 ---
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
try {
|
try {
|
||||||
await momentStore.fetchMoments()
|
await momentStore.fetchMoments()
|
||||||
@@ -311,7 +319,7 @@ async function deleteMoment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 辅助函数
|
// --- 原有辅助函数保留 ---
|
||||||
function getImageCount(moment: Moment): number {
|
function getImageCount(moment: Moment): number {
|
||||||
const urls = parseMediaUrls(moment.media_urls)
|
const urls = parseMediaUrls(moment.media_urls)
|
||||||
return Math.min(urls.length, 9)
|
return Math.min(urls.length, 9)
|
||||||
@@ -320,238 +328,272 @@ function getImageCount(moment: Moment): number {
|
|||||||
function getImageSize(moment: Moment): string {
|
function getImageSize(moment: Moment): string {
|
||||||
const count = getImageCount(moment)
|
const count = getImageCount(moment)
|
||||||
if (count === 1) return '400rpx'
|
if (count === 1) return '400rpx'
|
||||||
if (count <= 4) return '200rpx'
|
if (count === 4) return '200rpx'
|
||||||
return '150rpx'
|
return '180rpx'
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.page-container {
|
.moments-page {
|
||||||
min-height: 100vh;
|
/* 定义 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);
|
||||||
|
|
||||||
|
height: 100vh;
|
||||||
background: var(--bg-page);
|
background: var(--bg-page);
|
||||||
}
|
|
||||||
|
|
||||||
// 自定义导航栏
|
|
||||||
.custom-navbar {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 100;
|
|
||||||
background: transparent;
|
|
||||||
padding-top: var(--status-bar-height, 0);
|
|
||||||
|
|
||||||
.navbar-content {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
height: 88rpx;
|
|
||||||
padding: 0 30rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-title {
|
|
||||||
flex: 1;
|
|
||||||
font-size: 36rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #fff;
|
|
||||||
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-right {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 内容滚动区域
|
|
||||||
.content-scroll {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: calc(100rpx + env(safe-area-inset-bottom));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 封面区域
|
|
||||||
.cover-section {
|
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 500rpx;
|
|
||||||
|
|
||||||
.cover-bg {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-info {
|
|
||||||
position: absolute;
|
|
||||||
right: 30rpx;
|
|
||||||
bottom: -40rpx;
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-end;
|
|
||||||
gap: 16rpx;
|
|
||||||
|
|
||||||
.avatar-lg {
|
|
||||||
width: 120rpx;
|
|
||||||
height: 120rpx;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: 8rpx;
|
|
||||||
background: var(--color-primary);
|
|
||||||
color: #fff;
|
|
||||||
font-size: 48rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
border: 4rpx solid #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-name {
|
|
||||||
font-size: 32rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #fff;
|
|
||||||
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.5);
|
|
||||||
margin-bottom: 20rpx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 动态列表
|
.moments-page.dark {
|
||||||
.moments-list {
|
/* 调整深色模式颜色:
|
||||||
padding-top: 60rpx;
|
使用 #1c1c1e (iOS 深色背景) 替代纯黑 #000000
|
||||||
background: var(--bg-content);
|
提升质感,减少 OLED 拖影
|
||||||
min-height: calc(100vh - 500rpx);
|
*/
|
||||||
|
--bg-page: #1c1c1e;
|
||||||
|
--bg-nav-rgb: 28, 28, 30; /* 对应 #1c1c1e 的 RGB 值 */
|
||||||
|
--bg-detail: #2c2c2e; /* 稍微亮一点的二级背景,用于评论区 */
|
||||||
|
--text-main: #f2f2f7; /* 柔和的白色文字 */
|
||||||
|
--text-blue: #7e95c5;
|
||||||
|
--border-color: rgba(255,255,255,0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.moment-card {
|
/* 1. 沉浸式 Header */
|
||||||
|
.nav-header {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: calc(44px + var(--status-bar-height));
|
||||||
|
z-index: 100;
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 30rpx;
|
align-items: flex-end; /* 内容底部对齐 */
|
||||||
border-bottom: 1rpx solid var(--divider-color);
|
padding-bottom: 20rpx;
|
||||||
|
justify-content: center;
|
||||||
|
transition: background 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.moment-avatar {
|
.nav-content {
|
||||||
margin-right: 24rpx;
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
.avatar-placeholder {
|
.nav-title {
|
||||||
width: 80rpx;
|
font-weight: 600;
|
||||||
height: 80rpx;
|
font-size: 34rpx;
|
||||||
|
color: var(--text-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-right {
|
||||||
|
position: absolute;
|
||||||
|
right: 30rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-container {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 2. 封面区域 */
|
||||||
|
.cover-area {
|
||||||
|
position: relative;
|
||||||
|
height: 600rpx;
|
||||||
|
margin-bottom: 80rpx; /* 留出空间给头像 */
|
||||||
|
|
||||||
|
.cover-img { width: 100%; height: 100%; }
|
||||||
|
|
||||||
|
/* 底部渐变遮罩,保证文字清晰 */
|
||||||
|
.cover-gradient {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 160rpx;
|
||||||
|
background: linear-gradient(to top, var(--bg-page), transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-entry {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -40rpx;
|
||||||
|
right: 30rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 24rpx;
|
||||||
|
|
||||||
|
.username {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
text-shadow: 0 2rpx 4rpx rgba(0,0,0,0.5);
|
||||||
|
font-size: 36rpx;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-box {
|
||||||
|
border: 6rpx solid var(--bg-page);
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: var(--bg-page);
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 3. 动态列表 */
|
||||||
|
.feed-list {
|
||||||
|
padding: 0 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feed-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 20rpx;
|
||||||
|
margin-bottom: 60rpx;
|
||||||
|
border-bottom: 1rpx solid var(--border-color);
|
||||||
|
padding-bottom: 30rpx;
|
||||||
|
|
||||||
|
&:last-child { border-bottom: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.feed-avatar {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feed-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0; /* 防止文本溢出 */
|
||||||
|
|
||||||
|
.nickname {
|
||||||
|
color: var(--text-blue);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 32rpx;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-body {
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: var(--text-main);
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
display: block;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10rpx;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
|
||||||
|
.grid-img-wrap {
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-wrap {
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
.video-player {
|
||||||
|
width: 400rpx;
|
||||||
|
height: 300rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.feed-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 10rpx;
|
||||||
|
|
||||||
|
.time { font-size: 24rpx; color: #999; }
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
background: var(--bg-detail);
|
||||||
|
padding: 6rpx 16rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border-radius: 8rpx;
|
&:active { opacity: 0.6; }
|
||||||
color: #fff;
|
|
||||||
font-size: 32rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.moment-content {
|
/* 4. 交互区 (点赞评论) */
|
||||||
flex: 1;
|
.interaction-area {
|
||||||
min-width: 0;
|
margin-top: 20rpx;
|
||||||
}
|
background: var(--bg-detail);
|
||||||
|
|
||||||
.moment-username {
|
|
||||||
font-size: 30rpx;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--text-link);
|
|
||||||
margin-bottom: 12rpx;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.moment-text {
|
|
||||||
font-size: 30rpx;
|
|
||||||
color: var(--text-primary);
|
|
||||||
line-height: 1.6;
|
|
||||||
margin-bottom: 16rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.moment-images {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8rpx;
|
|
||||||
margin-bottom: 16rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.moment-video {
|
|
||||||
margin-bottom: 16rpx;
|
|
||||||
|
|
||||||
.video-player {
|
|
||||||
width: 100%;
|
|
||||||
height: 400rpx;
|
|
||||||
border-radius: 8rpx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.moment-footer {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 16rpx;
|
|
||||||
|
|
||||||
.moment-time {
|
|
||||||
font-size: 24rpx;
|
|
||||||
color: var(--text-tertiary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.moment-actions {
|
|
||||||
.action-item {
|
|
||||||
padding: 8rpx;
|
|
||||||
color: var(--text-tertiary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.moment-interactions {
|
|
||||||
background: var(--bg-hover);
|
|
||||||
border-radius: 8rpx;
|
border-radius: 8rpx;
|
||||||
padding: 16rpx;
|
padding: 16rpx 20rpx;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
.likes-row {
|
.triangle {
|
||||||
display: flex;
|
position: absolute;
|
||||||
align-items: center;
|
top: -12rpx;
|
||||||
gap: 8rpx;
|
left: 20rpx;
|
||||||
font-size: 26rpx;
|
width: 0; height: 0;
|
||||||
color: var(--text-link);
|
border-left: 12rpx solid transparent;
|
||||||
margin-bottom: 8rpx;
|
border-right: 12rpx solid transparent;
|
||||||
|
border-bottom: 12rpx solid var(--bg-detail);
|
||||||
.likes-text {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.likes-list {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: var(--text-blue);
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider { height: 1rpx; background: rgba(0,0,0,0.05); margin: 12rpx 0; }
|
||||||
|
|
||||||
.comment-row {
|
.comment-row {
|
||||||
font-size: 26rpx;
|
font-size: 28rpx;
|
||||||
color: var(--text-primary);
|
color: var(--text-main);
|
||||||
line-height: 1.8;
|
margin-bottom: 8rpx;
|
||||||
|
line-height: 1.5;
|
||||||
|
|
||||||
.comment-user {
|
&:last-child { margin-bottom: 0; }
|
||||||
color: var(--text-link);
|
.user-link { color: var(--text-blue); font-weight: 500; }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发布按钮
|
/* 5. 悬浮按钮 */
|
||||||
.fab {
|
.fab-btn {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: 30rpx;
|
bottom: calc(150rpx + env(safe-area-inset-bottom));
|
||||||
bottom: calc(180rpx + env(safe-area-inset-bottom));
|
right: 40rpx;
|
||||||
width: 100rpx;
|
width: 100rpx;
|
||||||
height: 100rpx;
|
height: 100rpx;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: var(--color-primary);
|
background: #07c160;
|
||||||
|
box-shadow: 0 10rpx 30rpx rgba(7, 193, 96, 0.3);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
box-shadow: 0 4rpx 20rpx rgba(7, 193, 96, 0.4);
|
|
||||||
z-index: 99;
|
z-index: 99;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
|
||||||
|
&:active { transform: scale(0.95); }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载状态
|
|
||||||
.loading-state {
|
.loading-state {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 100rpx;
|
padding: 100rpx;
|
||||||
color: var(--text-tertiary);
|
color: #999;
|
||||||
gap: 20rpx;
|
gap: 20rpx;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,109 +1,94 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="edit-page" :class="{ dark: isDark }">
|
<wd-config-provider :theme="isDark ? 'dark' : 'light'">
|
||||||
<!-- 导航栏 -->
|
<view class="edit-page" :class="{ dark: isDark }">
|
||||||
<app-nav-bar title="编辑资料" />
|
<app-nav-bar title="编辑资料" custom-style="background: transparent;" />
|
||||||
|
|
||||||
<view class="form-item">
|
<view class="content-body">
|
||||||
<input
|
<view class="input-card">
|
||||||
v-model="value"
|
<textarea
|
||||||
class="input"
|
v-if="field === 'desc'"
|
||||||
:placeholder="placeholder"
|
v-model="value"
|
||||||
:maxlength="maxLength"
|
class="custom-input textarea"
|
||||||
/>
|
:placeholder="placeholder"
|
||||||
<text v-if="maxLength" class="counter">{{ value.length }}/{{ maxLength }}</text>
|
:maxlength="maxLength"
|
||||||
|
placeholder-class="p-holder"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-else
|
||||||
|
v-model="value"
|
||||||
|
class="custom-input"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:maxlength="maxLength"
|
||||||
|
placeholder-class="p-holder"
|
||||||
|
/>
|
||||||
|
<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>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<button class="save-btn" @click="save">保存</button>
|
</wd-config-provider>
|
||||||
</view>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { useTheme } from '@/composables/useTheme'
|
import { useTheme } from '@/composables/useTheme'
|
||||||
|
|
||||||
const { isDark } = useTheme()
|
// --- 逻辑不变 ---
|
||||||
const field = ref('')
|
const { isDark } = useTheme(); const field = ref(''); const value = ref(''); const placeholder = ref(''); const maxLength = ref(0);
|
||||||
const value = ref('')
|
|
||||||
const placeholder = ref('')
|
|
||||||
const maxLength = ref(0)
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const pages = getCurrentPages()
|
const pages = getCurrentPages(); const currentPage = pages[pages.length - 1]; const options = (currentPage as any).$page?.options || {};
|
||||||
const currentPage = pages[pages.length - 1]
|
field.value = options.field || ''; value.value = decodeURIComponent(options.value || '');
|
||||||
const options = (currentPage as any).$page?.options || {}
|
|
||||||
|
|
||||||
field.value = options.field || ''
|
|
||||||
value.value = decodeURIComponent(options.value || '')
|
|
||||||
|
|
||||||
switch (field.value) {
|
switch (field.value) {
|
||||||
case 'name':
|
case 'name': placeholder.value = '请输入昵称'; maxLength.value = 20; uni.setNavigationBarTitle({ title: '修改昵称' }); break;
|
||||||
placeholder.value = '请输入昵称'
|
case 'desc': placeholder.value = '请输入个性签名'; maxLength.value = 100; uni.setNavigationBarTitle({ title: '修改签名' }); break;
|
||||||
maxLength.value = 20
|
case 'region': placeholder.value = '请输入地区'; maxLength.value = 50; uni.setNavigationBarTitle({ title: '修改地区' }); break;
|
||||||
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
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: 调用接口保存
|
|
||||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
|
||||||
setTimeout(() => {
|
|
||||||
uni.navigateBack()
|
|
||||||
}, 1500)
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.edit-page {
|
.edit-page {
|
||||||
padding: 30rpx;
|
--bg-page: #f5f7fa;
|
||||||
background: var(--bg-content);
|
--bg-surface: #ffffff;
|
||||||
|
--text-primary: #1d1d1f;
|
||||||
|
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
background: var(--bg-page);
|
||||||
|
}
|
||||||
|
.edit-page.dark {
|
||||||
|
--bg-page: #1c1c1e;
|
||||||
|
--bg-surface: #2c2c2e;
|
||||||
|
--text-primary: #f2f2f7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-item {
|
.content-body { padding: 40rpx; }
|
||||||
|
|
||||||
|
.input-card {
|
||||||
|
background: var(--bg-surface);
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 30rpx;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.02);
|
||||||
.input {
|
margin-bottom: 60rpx;
|
||||||
width: 100%;
|
|
||||||
padding: 24rpx;
|
|
||||||
background: var(--bg-page);
|
|
||||||
border-radius: 12rpx;
|
|
||||||
font-size: 32rpx;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.counter {
|
|
||||||
position: absolute;
|
|
||||||
right: 24rpx;
|
|
||||||
bottom: 24rpx;
|
|
||||||
font-size: 24rpx;
|
|
||||||
color: var(--text-tertiary);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.save-btn {
|
.custom-input {
|
||||||
margin-top: 60rpx;
|
width: 100%; font-size: 32rpx; color: var(--text-primary);
|
||||||
background: var(--color-primary);
|
&.textarea { height: 200rpx; }
|
||||||
color: #fff;
|
}
|
||||||
font-size: 32rpx;
|
|
||||||
border-radius: 12rpx;
|
|
||||||
|
|
||||||
&::after {
|
:deep(.p-holder) { color: #999; }
|
||||||
border: none;
|
|
||||||
}
|
.counter {
|
||||||
|
position: absolute; bottom: 20rpx; right: 30rpx;
|
||||||
|
font-size: 24rpx; color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-wrap {
|
||||||
|
margin-top: 40rpx;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,138 +1,139 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="profile-page" :class="{ dark: isDark }">
|
<wd-config-provider :theme="isDark ? 'dark' : 'light'">
|
||||||
<!-- 顶部导航栏 (透明背景或自定义颜色) -->
|
<view class="page-container" :class="{ dark: isDark }">
|
||||||
<app-nav-bar title="" :left-arrow="false" :bordered="false" />
|
|
||||||
|
|
||||||
<!-- 用户信息卡片 -->
|
<!-- 头部大卡片 -->
|
||||||
<view class="user-card" @click="goProfileEdit">
|
<view class="profile-header">
|
||||||
<view class="avatar-wrapper">
|
<view class="header-top">
|
||||||
<wd-img
|
<text class="page-title">我的</text>
|
||||||
:src="userInfo.avatar"
|
<view class="icon-btn" @click="uni.navigateTo({ url: '/pages/settings/index' })">
|
||||||
width="120rpx"
|
<wd-icon name="setting" size="44rpx" color="var(--text-primary)" />
|
||||||
height="120rpx"
|
</view>
|
||||||
round
|
</view>
|
||||||
mode="aspectFill"
|
|
||||||
/>
|
<view class="user-hero" @click="goProfileEdit">
|
||||||
</view>
|
<app-avatar :src="userInfo.avatar" :name="userInfo.nickname" :size="160" radius="50%" custom-style="border: 6rpx solid var(--bg-surface);" />
|
||||||
<view class="info-wrapper">
|
<view class="hero-info">
|
||||||
<view class="name-row">
|
<text class="nickname">{{ userInfo.nickname || '未设置昵称' }}</text>
|
||||||
<text class="nickname">{{ userInfo.nickname || '未设置昵称' }}</text>
|
<text class="account">账号: {{ userInfo.account || '--' }}</text>
|
||||||
<wd-icon name="edit" size="16px" color="var(--text-secondary)" custom-class="edit-icon" />
|
</view>
|
||||||
|
<wd-icon name="arrow-right" color="#999" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 数据概览 -->
|
||||||
|
<view class="stats-row">
|
||||||
|
<view class="stat-item">
|
||||||
|
<text class="num">12</text>
|
||||||
|
<text class="label">动态</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-item">
|
||||||
|
<text class="num">85</text>
|
||||||
|
<text class="label">好友</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-item">
|
||||||
|
<text class="num">3</text>
|
||||||
|
<text class="label">收藏</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<text class="account">账号: {{ userInfo.account || '--' }}</text>
|
|
||||||
</view>
|
</view>
|
||||||
<view class="arrow">
|
|
||||||
<wd-icon name="arrow-right" color="var(--text-tertiary)" />
|
<!-- 菜单列表 -->
|
||||||
|
<view class="menu-container">
|
||||||
|
<view class="menu-group">
|
||||||
|
<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>
|
||||||
|
<text class="menu-text">我的动态</text>
|
||||||
|
<wd-icon name="arrow-right" color="var(--text-tertiary)" size="28rpx" />
|
||||||
|
</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>
|
||||||
|
<text class="menu-text">我的收藏</text>
|
||||||
|
<wd-icon name="arrow-right" color="var(--text-tertiary)" size="28rpx" />
|
||||||
|
</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>
|
||||||
|
<text class="menu-text">相册</text>
|
||||||
|
<wd-icon name="arrow-right" color="var(--text-tertiary)" size="28rpx" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<app-tab-bar active="/pages/profile/index" />
|
||||||
</view>
|
</view>
|
||||||
|
</wd-config-provider>
|
||||||
<!-- 功能列表 -->
|
|
||||||
<view class="menu-list">
|
|
||||||
<wd-cell-group border>
|
|
||||||
<wd-cell title="我的动态" is-link to="/pages/moment/my" icon="moments" size="large" />
|
|
||||||
<wd-cell title="收藏夹" is-link to="/pages/collection/index" icon="star" size="large" />
|
|
||||||
<wd-cell title="相册" is-link to="/pages/album/index" icon="picture" size="large" />
|
|
||||||
</wd-cell-group>
|
|
||||||
|
|
||||||
<view class="gap" />
|
|
||||||
|
|
||||||
<wd-cell-group border>
|
|
||||||
<wd-cell title="设置" is-link to="/pages/settings/index" icon="setting" size="large" />
|
|
||||||
</wd-cell-group>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 底部 TabBar -->
|
|
||||||
<app-tab-bar active="/pages/profile/index" />
|
|
||||||
</view>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useTheme } from '@/composables/useTheme'
|
import { useTheme } from '@/composables/useTheme'
|
||||||
import { useAuthStore } from '@/stores'
|
import { useAuthStore } from '@/stores'
|
||||||
import AppNavBar from '@/components/common/AppNavBar.vue'
|
|
||||||
import AppTabBar from '@/components/common/AppTabBar.vue'
|
import AppTabBar from '@/components/common/AppTabBar.vue'
|
||||||
|
|
||||||
const { isDark } = useTheme()
|
const { isDark } = useTheme()
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
const userInfo = computed(() => authStore.userInfo || {})
|
const userInfo = computed(() => authStore.userInfo || {})
|
||||||
|
function goProfileEdit() { uni.navigateTo({ url: '/pages/profile/edit' }) }
|
||||||
function goProfileEdit() {
|
|
||||||
uni.navigateTo({ url: '/pages/profile/edit' })
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.profile-page {
|
.page-container {
|
||||||
|
--bg-page: #f5f7fa;
|
||||||
|
--bg-surface: #ffffff;
|
||||||
|
--text-primary: #1d1d1f;
|
||||||
|
--text-secondary: #86868b;
|
||||||
|
--text-tertiary: #9ca3af;
|
||||||
|
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background-color: var(--bg-page);
|
background: var(--bg-page);
|
||||||
padding-bottom: calc(100rpx + env(safe-area-inset-bottom));
|
padding-bottom: 200rpx;
|
||||||
|
}
|
||||||
|
.page-container.dark {
|
||||||
|
--bg-page: #1c1c1e; /* Dark Grey */
|
||||||
|
--bg-surface: #2c2c2e;
|
||||||
|
--text-primary: #f2f2f7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-card {
|
.profile-header {
|
||||||
display: flex;
|
background: var(--bg-surface);
|
||||||
align-items: center;
|
padding: calc(var(--status-bar-height) + 20rpx) 40rpx 60rpx;
|
||||||
padding: 40rpx 32rpx;
|
border-bottom-left-radius: 40rpx;
|
||||||
background-color: var(--bg-content);
|
border-bottom-right-radius: 40rpx;
|
||||||
margin-bottom: 24rpx;
|
box-shadow: 0 10rpx 30rpx rgba(0,0,0,0.03);
|
||||||
transition: background-color 0.2s;
|
}
|
||||||
|
|
||||||
&:active {
|
.header-top {
|
||||||
background-color: var(--bg-active);
|
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; }
|
||||||
|
}
|
||||||
|
|
||||||
.avatar-wrapper {
|
.user-hero {
|
||||||
margin-right: 32rpx;
|
display: flex; align-items: center; gap: 30rpx;
|
||||||
border: 2rpx solid var(--border-color);
|
.hero-info {
|
||||||
border-radius: 50%;
|
flex: 1; display: flex; flex-direction: column;
|
||||||
padding: 2rpx; // 模拟头像边框
|
.nickname { font-size: 40rpx; font-weight: 700; color: var(--text-primary); margin-bottom: 8rpx; }
|
||||||
}
|
.account { font-size: 26rpx; color: var(--text-secondary); }
|
||||||
|
|
||||||
.info-wrapper {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
.name-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 12rpx;
|
|
||||||
|
|
||||||
.nickname {
|
|
||||||
font-size: 40rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-primary);
|
|
||||||
margin-right: 12rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.edit-icon {
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.account {
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-list {
|
.stats-row {
|
||||||
.gap {
|
display: flex; justify-content: space-around; margin-top: 60rpx;
|
||||||
height: 24rpx;
|
.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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.menu-container { padding: 40rpx; }
|
||||||
.user-card {
|
.menu-group { background: var(--bg-surface); border-radius: 24rpx; padding: 10rpx 30rpx; }
|
||||||
background-color: var(--bg-content);
|
.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; }
|
||||||
:deep(.wd-cell) {
|
.icon-box {
|
||||||
background-color: var(--bg-content);
|
width: 70rpx; height: 70rpx; border-radius: 16rpx; display: flex; align-items: center; justify-content: center; margin-right: 24rpx;
|
||||||
color: var(--text-primary);
|
&.blue { background: #4f46e5; } &.yellow { background: #f59e0b; } &.green { background: #10b981; }
|
||||||
}
|
}
|
||||||
|
.menu-text { flex: 1; font-size: 30rpx; font-weight: 500; color: var(--text-primary); }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="search-page" :class="{ dark: isDark }">
|
<view class="search-page" :class="{ dark: isDark }">
|
||||||
<!-- 搜索栏 -->
|
<!-- 搜索栏 (增加 safe-area-top) -->
|
||||||
<view class="search-header safe-area-top">
|
<view class="search-header safe-area-top">
|
||||||
<wd-search
|
<wd-search
|
||||||
v-model="keyword"
|
v-model="keyword"
|
||||||
placeholder="搜索"
|
placeholder="搜索"
|
||||||
show-action
|
show-action
|
||||||
focus
|
focus
|
||||||
@search="doSearch"
|
@search="doSearch"
|
||||||
@cancel="goBack"
|
@cancel="goBack"
|
||||||
@clear="clearKeyword"
|
@clear="clearKeyword"
|
||||||
|
custom-style="background: transparent;"
|
||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -20,14 +21,14 @@
|
|||||||
<text class="clear" @click="clearHistory">清空</text>
|
<text class="clear" @click="clearHistory">清空</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="history-tags">
|
<view class="history-tags">
|
||||||
<wd-tag
|
<view
|
||||||
v-for="(item, index) in history"
|
v-for="(item, index) in history"
|
||||||
:key="index"
|
:key="index"
|
||||||
round
|
class="history-tag"
|
||||||
@click="searchHistory(item)"
|
@click="searchHistory(item)"
|
||||||
>
|
>
|
||||||
{{ item }}
|
{{ item }}
|
||||||
</wd-tag>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -41,25 +42,26 @@
|
|||||||
<wd-status-tip v-else-if="results.length === 0 && searched" tip="未找到相关结果" />
|
<wd-status-tip v-else-if="results.length === 0 && searched" tip="未找到相关结果" />
|
||||||
|
|
||||||
<template v-else-if="results.length > 0">
|
<template v-else-if="results.length > 0">
|
||||||
<wd-cell
|
<view class="result-list">
|
||||||
v-for="item in results"
|
<view
|
||||||
:key="item.id"
|
v-for="item in results"
|
||||||
:title="item.name"
|
:key="item.id"
|
||||||
:label="item.desc"
|
class="result-item"
|
||||||
is-link
|
@click="goDetail(item)"
|
||||||
@click="goDetail(item)"
|
>
|
||||||
>
|
<app-avatar
|
||||||
<template #icon>
|
|
||||||
<view class="result-avatar">
|
|
||||||
<app-avatar
|
|
||||||
:src="item.avatar"
|
:src="item.avatar"
|
||||||
:name="item.name"
|
:name="item.name"
|
||||||
:size="80"
|
:size="88"
|
||||||
radius="8rpx"
|
radius="12rpx"
|
||||||
/>
|
/>
|
||||||
|
<view class="result-info">
|
||||||
|
<text class="name">{{ item.name }}</text>
|
||||||
|
<text class="desc">{{ item.desc || '暂无签名' }}</text>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
<wd-icon name="arrow-right" size="32rpx" color="var(--text-tertiary)" />
|
||||||
</wd-cell>
|
</view>
|
||||||
|
</view>
|
||||||
</template>
|
</template>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -161,60 +163,67 @@ function goDetail(item: User) {
|
|||||||
.search-page {
|
.search-page {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: var(--bg-page);
|
background: var(--bg-page);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-header {
|
.search-header {
|
||||||
background: var(--bg-content);
|
background: var(--bg-content);
|
||||||
|
padding: 10rpx 20rpx;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
.safe-area-top {
|
.safe-area-top {
|
||||||
padding-top: var(--status-bar-height, 0);
|
padding-top: var(--status-bar-height);
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-section {
|
.history-section {
|
||||||
background: var(--bg-content);
|
|
||||||
margin-top: 20rpx;
|
|
||||||
padding: 30rpx;
|
padding: 30rpx;
|
||||||
|
|
||||||
.section-header {
|
.section-header {
|
||||||
display: flex;
|
display: flex; justify-content: space-between; margin-bottom: 24rpx;
|
||||||
justify-content: space-between;
|
.title { font-size: 28rpx; font-weight: 600; color: var(--text-primary); }
|
||||||
margin-bottom: 24rpx;
|
.clear { font-size: 26rpx; color: var(--text-tertiary); }
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: var(--text-tertiary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.clear {
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: var(--text-tertiary);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-tags {
|
.history-tags {
|
||||||
display: flex;
|
display: flex; flex-wrap: wrap; gap: 20rpx;
|
||||||
flex-wrap: wrap;
|
.history-tag {
|
||||||
gap: 16rpx;
|
background: var(--bg-content); padding: 10rpx 24rpx; border-radius: 30rpx;
|
||||||
|
font-size: 26rpx; color: var(--text-secondary);
|
||||||
|
&:active { opacity: 0.7; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.results-section {
|
.results-section {
|
||||||
background: var(--bg-content);
|
flex: 1;
|
||||||
margin-top: 20rpx;
|
background: var(--bg-page);
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-state {
|
.loading-state {
|
||||||
display: flex;
|
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||||
flex-direction: column;
|
padding-top: 100rpx; color: var(--text-tertiary); gap: 20rpx;
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 60rpx;
|
|
||||||
color: var(--text-tertiary);
|
|
||||||
gap: 20rpx;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-avatar {
|
.result-list {
|
||||||
margin-right: 24rpx;
|
padding: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item {
|
||||||
|
display: flex; align-items: center;
|
||||||
|
background: var(--bg-content);
|
||||||
|
padding: 24rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
|
||||||
|
&:active { opacity: 0.8; }
|
||||||
|
|
||||||
|
.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; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,37 +1,47 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="settings-page" :class="{ dark: isDark }">
|
<wd-config-provider :theme="isDark ? 'dark' : 'light'">
|
||||||
<app-nav-bar title="设置" />
|
<view class="settings-page" :class="{ dark: isDark }">
|
||||||
|
<app-nav-bar title="设置" custom-style="background: transparent;" />
|
||||||
|
|
||||||
<view class="content">
|
<view class="content-body">
|
||||||
<!-- 账号安全 -->
|
<!-- 账号安全 -->
|
||||||
<wd-cell-group title="账号安全" border>
|
<view class="settings-group">
|
||||||
<wd-cell title="账号管理" is-link to="/pages/settings/account" />
|
<view class="group-title">账号</view>
|
||||||
<wd-cell title="隐私设置" is-link to="/pages/settings/privacy" />
|
<view class="group-card">
|
||||||
</wd-cell-group>
|
<wd-cell title="账号管理" is-link to="/pages/settings/account" size="large" />
|
||||||
|
<wd-cell title="隐私设置" is-link to="/pages/settings/privacy" size="large" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<!-- 通用设置 -->
|
<!-- 通用设置 -->
|
||||||
<wd-cell-group title="通用" border>
|
<view class="settings-group">
|
||||||
<wd-cell title="新消息通知" is-link to="/pages/settings/notification" />
|
<view class="group-title">通用</view>
|
||||||
<wd-cell title="主题设置" is-link to="/pages/settings/theme" value="自动" />
|
<view class="group-card">
|
||||||
<wd-cell title="通用设置" is-link to="/pages/settings/general" />
|
<wd-cell title="新消息通知" is-link to="/pages/settings/notification" size="large" />
|
||||||
<wd-cell title="清理缓存" is-link value="12.5MB" clickable @click="handleClearCache" />
|
<wd-cell title="主题设置" is-link to="/pages/settings/theme" value="自动" size="large" />
|
||||||
</wd-cell-group>
|
<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>
|
||||||
|
</view>
|
||||||
|
|
||||||
<!-- 关于 -->
|
<!-- 关于 -->
|
||||||
<wd-cell-group title="关于" border>
|
<view class="settings-group">
|
||||||
<wd-cell title="帮助与反馈" is-link to="/pages/settings/feedback" />
|
<view class="group-card">
|
||||||
<wd-cell title="关于我们" is-link to="/pages/settings/about" value="v1.0.0" />
|
<wd-cell title="帮助与反馈" is-link to="/pages/settings/feedback" size="large" />
|
||||||
</wd-cell-group>
|
<wd-cell title="关于我们" is-link to="/pages/settings/about" value="v1.0.0" size="large" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<!-- 退出登录 -->
|
<!-- 退出登录 -->
|
||||||
<view class="logout-section">
|
<view class="logout-btn" @click="handleLogout">
|
||||||
<wd-button type="error" block size="large" @click="handleLogout">退出登录</wd-button>
|
<text>退出登录</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
|
||||||
|
|
||||||
<wd-toast />
|
<wd-toast />
|
||||||
<wd-message-box />
|
<wd-message-box />
|
||||||
</view>
|
</view>
|
||||||
|
</wd-config-provider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -40,63 +50,46 @@ import { useAuthStore } from '@/stores'
|
|||||||
import { useToast, useMessage } from 'wot-design-uni'
|
import { useToast, useMessage } from 'wot-design-uni'
|
||||||
import AppNavBar from '@/components/common/AppNavBar.vue'
|
import AppNavBar from '@/components/common/AppNavBar.vue'
|
||||||
|
|
||||||
const { isDark } = useTheme()
|
// --- 逻辑不变 ---
|
||||||
const authStore = useAuthStore()
|
const { isDark } = useTheme(); const authStore = useAuthStore(); const toast = useToast(); const message = useMessage();
|
||||||
const toast = useToast()
|
function handleLogout() { message.confirm({ title: '提示', msg: '确定要退出登录吗?', success: async () => { try { await authStore.logout(); uni.reLaunch({ url: '/pages/login/index' }) } catch (error) { toast.error('退出失败') } } }) }
|
||||||
const message = useMessage()
|
function handleClearCache() { toast.loading('清理中...'); setTimeout(() => { toast.success('清理完成') }, 1000) }
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.settings-page {
|
.settings-page {
|
||||||
|
--bg-page: #f5f7fa;
|
||||||
|
--bg-surface: #ffffff;
|
||||||
|
--text-secondary: #86868b;
|
||||||
|
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background-color: var(--bg-page);
|
background: var(--bg-page);
|
||||||
|
|
||||||
.content {
|
|
||||||
padding-bottom: 40rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logout-section {
|
|
||||||
margin: 60rpx 30rpx 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 暗黑模式微调 Cell Group 标题颜色
|
.settings-page.dark {
|
||||||
.dark {
|
--bg-page: #1c1c1e;
|
||||||
:deep(.wd-cell-group__title) {
|
--bg-surface: #2c2c2e;
|
||||||
color: var(--text-secondary);
|
--text-secondary: #aeaeb2;
|
||||||
background-color: var(--bg-page);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.wd-cell) {
|
.content-body { padding: 30rpx; }
|
||||||
background-color: var(--bg-content);
|
|
||||||
color: var(--text-primary);
|
|
||||||
|
|
||||||
// 覆盖 cell 内部线条颜色 (如果需要)
|
.settings-group { margin-bottom: 40rpx; }
|
||||||
&::after {
|
.group-title { font-size: 26rpx; color: var(--text-secondary); margin-bottom: 16rpx; margin-left: 20rpx; }
|
||||||
background-color: var(--border-color);
|
.group-card {
|
||||||
}
|
border-radius: 24rpx; overflow: hidden;
|
||||||
}
|
/* 覆盖 Cell 背景 */
|
||||||
|
:deep(.wd-cell) { background-color: var(--bg-surface); }
|
||||||
|
:deep(.wd-cell__title) { font-weight: 500; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-btn {
|
||||||
|
background: var(--bg-surface);
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 30rpx;
|
||||||
|
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); }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,56 +1,44 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="theme-page" :class="{ dark: isDark }">
|
<wd-config-provider :theme="isDark ? 'dark' : 'light'">
|
||||||
<!-- 导航栏 -->
|
<view class="theme-page" :class="{ dark: isDark }">
|
||||||
<app-nav-bar title="深色模式" />
|
<app-nav-bar title="主题设置" custom-style="background: transparent;" />
|
||||||
|
|
||||||
<wd-cell-group title="选择主题模式">
|
|
||||||
<wd-cell
|
|
||||||
title="跟随系统"
|
|
||||||
center
|
|
||||||
clickable
|
|
||||||
@click="selectTheme('system')"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<wd-icon name="setting" size="40rpx" class="cell-icon" />
|
|
||||||
</template>
|
|
||||||
<template #value>
|
|
||||||
<wd-icon v-if="currentTheme === 'system'" name="check" size="40rpx" color="var(--color-primary)" />
|
|
||||||
</template>
|
|
||||||
</wd-cell>
|
|
||||||
<wd-cell
|
|
||||||
title="浅色模式"
|
|
||||||
center
|
|
||||||
clickable
|
|
||||||
@click="selectTheme('light')"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<wd-icon name="sun" size="40rpx" class="cell-icon" />
|
|
||||||
</template>
|
|
||||||
<template #value>
|
|
||||||
<wd-icon v-if="currentTheme === 'light'" name="check" size="40rpx" color="var(--color-primary)" />
|
|
||||||
</template>
|
|
||||||
</wd-cell>
|
|
||||||
<wd-cell
|
|
||||||
title="深色模式"
|
|
||||||
center
|
|
||||||
clickable
|
|
||||||
@click="selectTheme('dark')"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<wd-icon name="moon" size="40rpx" class="cell-icon" />
|
|
||||||
</template>
|
|
||||||
<template #value>
|
|
||||||
<wd-icon v-if="currentTheme === 'dark'" name="check" size="40rpx" color="var(--color-primary)" />
|
|
||||||
</template>
|
|
||||||
</wd-cell>
|
|
||||||
</wd-cell-group>
|
|
||||||
|
|
||||||
<view class="tips">
|
<view class="content-body">
|
||||||
<text>「跟随系统」会根据系统设置自动切换深色/浅色模式</text>
|
<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>
|
||||||
|
<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>
|
||||||
|
<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">
|
||||||
|
<text>「跟随系统」开启后,将根据系统显示设置自动切换深色或浅色模式。</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<wd-toast />
|
||||||
</view>
|
</view>
|
||||||
|
</wd-config-provider>
|
||||||
<wd-toast />
|
|
||||||
</view>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -58,70 +46,70 @@ import { ref, onMounted } from 'vue'
|
|||||||
import { useTheme } from '@/composables/useTheme'
|
import { useTheme } from '@/composables/useTheme'
|
||||||
import { useToast } from 'wot-design-uni'
|
import { useToast } from 'wot-design-uni'
|
||||||
|
|
||||||
const { isDark, setTheme: applyTheme } = useTheme()
|
// --- 逻辑不变 ---
|
||||||
const toast = useToast()
|
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 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
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
function selectTheme(mode: 'system' | 'light' | 'dark') {
|
function selectTheme(mode: 'system' | 'light' | 'dark') {
|
||||||
console.log('selectTheme called:', mode)
|
currentTheme.value = mode; uni.setStorageSync('nl_im_theme_mode', mode);
|
||||||
currentTheme.value = mode
|
if (mode === 'system') { uni.removeStorageSync('nl_im_theme'); let systemDark = false; try { systemDark = uni.getSystemInfoSync().theme === 'dark' } catch (e) {} applyTheme(systemDark, false) }
|
||||||
uni.setStorageSync('nl_im_theme_mode', mode)
|
else { applyTheme(mode === 'dark', true) }
|
||||||
|
const tips = { system: '已设置为跟随系统', light: '已切换为浅色模式', dark: '已切换为深色模式' }; toast.success(tips[mode])
|
||||||
// 应用主题
|
|
||||||
if (mode === 'system') {
|
|
||||||
// 跟随系统 - 清除手动设置,不持久化
|
|
||||||
uni.removeStorageSync('nl_im_theme')
|
|
||||||
let systemDark = false
|
|
||||||
try {
|
|
||||||
systemDark = uni.getSystemInfoSync().theme === 'dark'
|
|
||||||
} catch (e) {
|
|
||||||
console.log('获取系统主题失败:', e)
|
|
||||||
}
|
|
||||||
console.log('系统深色模式:', systemDark)
|
|
||||||
applyTheme(systemDark, false) // 不持久化
|
|
||||||
} else {
|
|
||||||
console.log('设置主题:', mode === 'dark' ? '深色' : '浅色')
|
|
||||||
applyTheme(mode === 'dark', true) // 持久化
|
|
||||||
}
|
|
||||||
|
|
||||||
const tips = {
|
|
||||||
system: '已设置为跟随系统',
|
|
||||||
light: '已切换为浅色模式',
|
|
||||||
dark: '已切换为深色模式'
|
|
||||||
}
|
|
||||||
toast.success(tips[mode])
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.theme-page {
|
.theme-page {
|
||||||
|
--bg-page: #f5f7fa;
|
||||||
|
--bg-surface: #ffffff;
|
||||||
|
--text-primary: #1d1d1f;
|
||||||
|
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: var(--bg-page);
|
background: var(--bg-page);
|
||||||
}
|
}
|
||||||
|
.theme-page.dark {
|
||||||
.cell-icon {
|
--bg-page: #1c1c1e;
|
||||||
margin-right: 16rpx;
|
--bg-surface: #2c2c2e;
|
||||||
color: var(--text-secondary);
|
--text-primary: #f2f2f7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tips {
|
.content-body { padding: 40rpx; }
|
||||||
padding: 30rpx;
|
|
||||||
|
.option-list {
|
||||||
text {
|
display: grid; grid-template-columns: 1fr 1fr; gap: 30rpx; margin-bottom: 40rpx;
|
||||||
font-size: 26rpx;
|
|
||||||
color: var(--text-tertiary);
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
|
||||||
|
&.active { border-color: #4f46e5; background: rgba(79, 70, 229, 0.05); }
|
||||||
|
|
||||||
|
.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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 简单的 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; }
|
||||||
|
}
|
||||||
|
.light-prev {
|
||||||
|
background: #fff;
|
||||||
|
.p-nav { background: #f7f7f7; } .p-body { background: #eee; }
|
||||||
|
}
|
||||||
|
.dark-prev {
|
||||||
|
background: #1c1c1e; border-color: #333;
|
||||||
|
.p-nav { background: #2c2c2e; border-color: #333; } .p-body { background: #333; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips { padding: 0 10rpx; font-size: 24rpx; color: #999; line-height: 1.5; }
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -59,53 +59,49 @@ page {
|
|||||||
// 开关
|
// 开关
|
||||||
--switch-on: #07c160;
|
--switch-on: #07c160;
|
||||||
--switch-off: #e5e5e5;
|
--switch-off: #e5e5e5;
|
||||||
|
|
||||||
|
// 状态栏高度变量 (小程序/App会自动注入,H5需fallback)
|
||||||
|
--status-bar-height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 暗色主题 (支持多种选择器以兼容 H5)
|
// 暗色主题 (Dark Mode - 高级灰优化)
|
||||||
page.dark,
|
page.dark,
|
||||||
.dark,
|
.dark,
|
||||||
html.dark,
|
html.dark,
|
||||||
body.dark {
|
body.dark {
|
||||||
// 背景色
|
// 背景色 - 使用 #1c1c1e 替代纯黑
|
||||||
--bg-page: #111111;
|
--bg-page: #1c1c1e;
|
||||||
--bg-content: #1e1e1e;
|
--bg-content: #2c2c2e;
|
||||||
--bg-hover: #2a2a2a;
|
--bg-hover: #3a3a3c;
|
||||||
--bg-mask: rgba(0, 0, 0, 0.7);
|
--bg-mask: rgba(0, 0, 0, 0.7);
|
||||||
--bg-card: #1e1e1e;
|
--bg-card: #2c2c2e;
|
||||||
|
|
||||||
// 文字颜色
|
// 文字颜色
|
||||||
--text-primary: #e5e5e5;
|
--text-primary: #f2f2f7; // 柔和白
|
||||||
--text-secondary: #a0a0a0;
|
--text-secondary: #aeaeb2;
|
||||||
--text-tertiary: #666666;
|
--text-tertiary: #636366;
|
||||||
--text-placeholder: #444444;
|
--text-placeholder: #505055;
|
||||||
--text-link: #7b9ad0;
|
--text-link: #7b9ad0;
|
||||||
|
|
||||||
// 边框颜色
|
// 边框颜色
|
||||||
--border-color: #333333;
|
--border-color: #38383a;
|
||||||
--divider-color: #2a2a2a;
|
--divider-color: #38383a;
|
||||||
|
|
||||||
// 导航栏
|
// 导航栏
|
||||||
--nav-bg: #1a1a1a;
|
--nav-bg: #1c1c1e;
|
||||||
--nav-text: #ffffff;
|
--nav-text: #ffffff;
|
||||||
|
|
||||||
// 标签栏
|
// 标签栏
|
||||||
--tabbar-bg: #1a1a1a;
|
--tabbar-bg: #1c1c1e;
|
||||||
|
|
||||||
// 消息气泡
|
// 消息气泡
|
||||||
--bubble-self: #2d5c2e;
|
--bubble-self: #2d5c2e; // 深绿
|
||||||
--bubble-other: #2a2a2a;
|
--bubble-other: #2c2c2e;
|
||||||
--bubble-system: rgba(255, 255, 255, 0.1);
|
--bubble-system: rgba(255, 255, 255, 0.1);
|
||||||
|
|
||||||
// 搜索框
|
// 搜索框
|
||||||
--search-bg: #2a2a2a;
|
--search-bg: #2c2c2e;
|
||||||
|
|
||||||
// 开关
|
// 开关
|
||||||
--switch-off: #444444;
|
--switch-off: #3a3a3c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user