修复一些问题

This commit is contained in:
李琦
2026-07-09 08:53:36 +08:00
parent 497d0a840f
commit 5d6f34ecee
6 changed files with 500 additions and 229 deletions

View File

@@ -133,7 +133,7 @@ function handleLogout() {
background: rgba(0, 0, 0, 0.2); background: rgba(0, 0, 0, 0.2);
backdrop-filter: blur(2px); backdrop-filter: blur(2px);
-webkit-backdrop-filter: blur(2px); -webkit-backdrop-filter: blur(2px);
z-index: 1000; z-index: 1;
} }
.mask-enter { .mask-enter {
@@ -156,7 +156,7 @@ function handleLogout() {
width: 75%; width: 75%;
height: 100%; height: 100%;
background: var(--drawer-bg); background: var(--drawer-bg);
z-index: 1001; z-index: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
box-shadow: 16rpx 0 48rpx rgba(0, 0, 0, 0.1); box-shadow: 16rpx 0 48rpx rgba(0, 0, 0, 0.1);

View File

@@ -129,7 +129,7 @@ function switchTab(name: string) {
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
z-index: 10; z-index: 1;
height: 168rpx; height: 168rpx;
background: #ffffff; background: #ffffff;
border-top: 1rpx solid rgba(0, 0, 0, 0.05); border-top: 1rpx solid rgba(0, 0, 0, 0.05);

View File

@@ -171,7 +171,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, nextTick, getCurrentInstance } from 'vue' import { ref, computed, onMounted, onUnmounted, nextTick, getCurrentInstance } from 'vue'
import { onLoad } from '@dcloudio/uni-app' import { onLoad, onReady } from '@dcloudio/uni-app'
import { useAuthStore, useChatStore, useConversationStore } from '@/stores' import { useAuthStore, useChatStore, useConversationStore } from '@/stores'
import { wsManager } from '@/api/websocket' import { wsManager } from '@/api/websocket'
import * as messageApi from '@/api/modules/message' import * as messageApi from '@/api/modules/message'
@@ -219,8 +219,12 @@ const instance = getCurrentInstance()
// 状态定义 // 状态定义
const roomId = ref(''); const targetId = ref(''); const chatName = ref(''); const targetAvatar = ref(''); const roomId = ref(''); const targetId = ref(''); const chatName = ref(''); const targetAvatar = ref('');
const inputText = ref(''); const messages = ref<ChatMessage[]>([]); const scrollToId = ref(''); const inputText = ref(''); const messages = ref<ChatMessage[]>([]); const scrollToId = ref('');
const scrollTop = ref(0); // 增加 scrollTop 控制 const scrollTop = ref(0)
const scrollWithAnimation = ref(false); const loadingMore = ref(false); const hasMore = ref(true); const scrollTopSeed = ref(0)
const scrollWithAnimation = ref(false)
const loadingMore = ref(false)
const hasMore = ref(true)
let scrollRetryTimers: ReturnType<typeof setTimeout>[] = []
const playingAudioId = ref<number | null>(null); const page = ref(1); const playingAudioId = ref<number | null>(null); const page = ref(1);
const showFileConfirm = ref(false); const uploading = ref(false); const pendingFile = ref<{ path: string; name: string; size: number; type: 'image' | 'video' | 'file'; messageType: number }>({ path: '', name: '', size: 0, type: 'file', messageType: 8 }); const showFileConfirm = ref(false); const uploading = ref(false); const pendingFile = ref<{ path: string; name: string; size: number; type: 'image' | 'video' | 'file'; messageType: number }>({ path: '', name: '', size: 0, type: 'file', messageType: 8 });
const showMsgActions = ref(false); const selectedMessage = ref<ChatMessage | null>(null); const showMsgActions = ref(false); const selectedMessage = ref<ChatMessage | null>(null);
@@ -248,42 +252,52 @@ onLoad(async (options: any) => {
await loadGroupMembers() await loadGroupMembers()
} }
loadMessages(); loadMessages()
}); })
onUnmounted(() => { wsManager.offMessage(handleNewMessage); stopAudio(); }); onReady(() => {
scrollToBottom(false)
})
// ---------------------------------------------------------------------- onUnmounted(() => {
// 1. 终极滚动修复:组合 scroll-into-view 和 scrollTop clearScrollRetries()
// ---------------------------------------------------------------------- wsManager.offMessage(handleNewMessage)
function scrollToBottom(animated = true) { stopAudio()
scrollWithAnimation.value = animated })
// 第一招:使用 scroll-into-view 锚点 /** 清除滚动重试定时器,避免页面卸载后仍触发 */
// 先清空,确保下次设置能触发变化 function clearScrollRetries() {
scrollToId.value = '' scrollRetryTimers.forEach(t => clearTimeout(t))
scrollRetryTimers = []
nextTick(() => {
setTimeout(() => {
// 尝试1滚动到最后一条消息
if (messages.value.length > 0) {
scrollToId.value = `msg-${messages.value[messages.value.length - 1].id}`
} else {
scrollToId.value = 'msg-bottom'
} }
console.log('🚀 触发滚动 ID:', scrollToId.value) /** 执行一次滚底:锚点 msg-bottom + scrollTop 递增 */
function applyScrollToBottom() {
// 第二招:双保险,直接设置 scrollTop 到一个巨大值 scrollToId.value = 'msg-bottom'
// 某些情况下 view 没渲染出来 id 找不到scrollTop 更可靠
uni.createSelectorQuery().in(instance).select('.message-feed').boundingClientRect((res) => { uni.createSelectorQuery().in(instance).select('.message-feed').boundingClientRect((res) => {
if (res && res.height) { if (res && res.height) {
console.log('📏 计算高度滚动:', res.height) scrollTop.value = 0
scrollTop.value = res.height + 1000 // 加个 buffer 确保滚到底 nextTick(() => {
scrollTopSeed.value++
scrollTop.value = res.height + scrollTopSeed.value
})
} }
}).exec() }).exec()
}
}, 150) // 延时稍微加大一点,等待渲染完全 /**
* 滚动到消息列表底部
* 组合 scroll-into-view 与 scrollTop并在多个延迟点重试以应对图片/群成员渲染
*/
function scrollToBottom(animated = true, retries = [0, 100, 300]) {
scrollWithAnimation.value = animated
clearScrollRetries()
scrollToId.value = ''
retries.forEach((delay) => {
const timer = setTimeout(() => {
nextTick(() => applyScrollToBottom())
}, delay)
scrollRetryTimers.push(timer)
}) })
} }
@@ -369,12 +383,10 @@ async function loadMessages() {
const res = await messageApi.getMessages(roomId.value, 1, 50); const res = await messageApi.getMessages(roomId.value, 1, 50);
const newMsgs = mapMessages((res.data || []).reverse()); const newMsgs = mapMessages((res.data || []).reverse());
if (newMsgs.length > cached.length || cached.length === 0) { messages.value = newMsgs
messages.value = newMsgs; chatStore.setRoomMessages(roomId.value, messages.value)
chatStore.setRoomMessages(roomId.value, messages.value); scrollToBottom(false)
scrollToBottom(false); hasMore.value = res.data.length >= 50
}
hasMore.value = res.data.length >= 50;
} catch (error) { console.error('加载消息失败:', error) } } catch (error) { console.error('加载消息失败:', error) }
} }
@@ -442,6 +454,7 @@ async function loadGroupMembers() {
} }
}) })
groupMembersMap.value = memberMap groupMembersMap.value = memberMap
scrollToBottom(false)
} catch (e) { } catch (e) {
console.error('加载群成员失败:', e) console.error('加载群成员失败:', e)
} finally { } finally {

View File

@@ -1,6 +1,12 @@
<template> <template>
<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 }"
@touchstart="onDrawerSwipeStart"
@touchmove="onDrawerSwipeMove"
@touchend="onDrawerSwipeEnd"
>
<!-- 1. 顶部导航栏 (与设计稿完全一致) --> <!-- 1. 顶部导航栏 (与设计稿完全一致) -->
<view class="custom-navbar"> <view class="custom-navbar">
@@ -184,11 +190,13 @@
:z-index="9999" :z-index="9999"
/> />
<!-- 侧边抽屉 --> <!-- 侧边抽屉独立层叠上下文避免 z-index 过高遮挡全局弹窗 -->
<view class="drawer-layer">
<app-drawer <app-drawer
v-model="showDrawer" v-model="showDrawer"
@logout="logout" @logout="logout"
/> />
</view>
<wd-toast /> <wd-toast />
<wd-message-box :z-index="11000" /> <wd-message-box :z-index="11000" />
@@ -224,6 +232,12 @@ const refreshing = ref(false)
const showConvActions = ref(false) const showConvActions = ref(false)
const selectedConv = ref<Conversation | null>(null) const selectedConv = ref<Conversation | null>(null)
/** 左边缘右滑打开抽屉的手势状态 */
let swipeStartX = 0
let swipeStartY = 0
let swipeFromEdge = false
let swipeTriggered = false
const user = computed(() => authStore.user) const user = computed(() => authStore.user)
const conversations = computed(() => conversationStore.conversations) const conversations = computed(() => conversationStore.conversations)
const loading = computed(() => conversationStore.loading) const loading = computed(() => conversationStore.loading)
@@ -323,6 +337,32 @@ function onPlusMenuSelect(key: string) {
function openDrawer() { showDrawer.value = true } function openDrawer() { showDrawer.value = true }
/** 从左边缘右滑打开抽屉 */
function onDrawerSwipeStart(e: TouchEvent) {
if (showDrawer.value) return
const touch = e.touches[0]
swipeStartX = touch.clientX
swipeStartY = touch.clientY
swipeFromEdge = touch.clientX < 40
swipeTriggered = false
}
function onDrawerSwipeMove(e: TouchEvent) {
if (!swipeFromEdge || showDrawer.value || swipeTriggered) return
const touch = e.touches[0]
const deltaX = touch.clientX - swipeStartX
const deltaY = Math.abs(touch.clientY - swipeStartY)
if (deltaX > 60 && deltaY < 30) {
swipeTriggered = true
openDrawer()
}
}
function onDrawerSwipeEnd() {
swipeFromEdge = false
swipeTriggered = false
}
async function logout() { async function logout() {
try { try {
await messageBox.confirm({ title: '提示', msg: '确定退出?' }) await messageBox.confirm({ title: '提示', msg: '确定退出?' })
@@ -354,6 +394,12 @@ async function logout() {
min-height: 100vh; min-height: 100vh;
background: var(--bg-page); background: var(--bg-page);
color: var(--text-primary); color: var(--text-primary);
position: relative;
}
.drawer-layer {
position: relative;
z-index: 10;
} }
// ========================================== // ==========================================
@@ -401,7 +447,7 @@ async function logout() {
.custom-navbar { .custom-navbar {
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 100; z-index: 1;
// 确保没有任何边框 // 确保没有任何边框
border: none; border: none;

View File

@@ -4,128 +4,196 @@
<!-- 导航栏 --> <!-- 导航栏 -->
<view class="nav-bar"> <view class="nav-bar">
<view class="nav-back" @click="goBack"> <view class="nav-back" @click="goBack">
<!-- #ifdef H5 || APP-PLUS -->
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M15 18l-6-6 6-6"/> <path d="M15 18l-6-6 6-6"/>
</svg> </svg>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<wd-icon name="arrow-left" size="44rpx" />
<!-- #endif -->
</view> </view>
<text class="nav-title">{{ pageTitle }}</text> <text class="nav-title">编辑资料</text>
<view class="nav-placeholder"></view> <view class="nav-placeholder"></view>
</view> </view>
<view class="content-body"> <view class="content-body">
<!-- 输入卡片 --> <!-- 头像编辑区 -->
<view class="input-card animate-fade-in-up"> <view class="avatar-section animate-fade-in-up">
<view class="input-label">{{ inputLabel }}</view> <view class="avatar-wrap" @click="chooseAvatar">
<textarea <app-avatar :src="form.avatar" :name="form.name" :size="192" round />
v-if="field === 'desc'" <view class="avatar-mask">
v-model="value" <!-- #ifdef H5 || APP-PLUS -->
class="custom-input textarea" <svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2">
:placeholder="placeholder" <path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/>
:maxlength="maxLength" <circle cx="12" cy="13" r="4"/>
placeholder-class="p-holder" </svg>
/> <!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<wd-icon name="camera" size="64rpx" color="#fff" />
<!-- #endif -->
</view>
<view class="avatar-badge">
<!-- #ifdef H5 || APP-PLUS -->
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
</svg>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<wd-icon name="edit" size="24rpx" color="#fff" />
<!-- #endif -->
</view>
</view>
<text class="avatar-hint">点击更换头像</text>
</view>
<!-- 基础信息 -->
<view class="info-card animate-fade-in-up" style="animation-delay: 80ms;">
<view class="field-row">
<text class="field-label">昵称</text>
<input <input
v-else v-model="form.name"
v-model="value" class="field-input"
class="custom-input" placeholder="请输入昵称"
:placeholder="placeholder" maxlength="20"
:maxlength="maxLength"
placeholder-class="p-holder" placeholder-class="p-holder"
/> />
<text v-if="maxLength" class="counter">{{ value.length }}/{{ maxLength }}</text> </view>
<view class="field-row no-border">
<text class="field-label">地区</text>
<input
v-model="form.region"
class="field-input"
placeholder="请输入地区"
maxlength="50"
placeholder-class="p-holder"
/>
</view>
</view>
<!-- 个性签名 -->
<view class="desc-card animate-fade-in-up" style="animation-delay: 120ms;">
<text class="desc-label">个性签名</text>
<textarea
v-model="form.desc"
class="desc-input"
placeholder="介绍一下自己吧..."
maxlength="100"
placeholder-class="p-holder"
/>
<text class="counter">{{ form.desc.length }}/100</text>
</view> </view>
<!-- 保存按钮 --> <!-- 保存按钮 -->
<view class="btn-wrap animate-fade-in-up" style="animation-delay: 100ms;"> <view class="btn-wrap animate-fade-in-up" style="animation-delay: 160ms;">
<view class="save-btn" @click="save"> <view class="save-btn" :class="{ disabled: saving }" @click="save">
保存 {{ saving ? '保存中...' : '保存修改' }}
</view> </view>
</view> </view>
</view> </view>
<wd-toast />
</view> </view>
</wd-config-provider> </wd-config-provider>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { reactive, ref, onMounted } from 'vue'
import { useTheme } from '@/composables/useTheme' import { useTheme } from '@/composables/useTheme'
import { useAuthStore } from '@/stores' import { useAuthStore } from '@/stores'
import * as userApi from '@/api/modules/user' import * as userApi from '@/api/modules/user'
import * as attachmentApi from '@/api/modules/attachment'
import { useToast } from 'wot-design-uni'
import AppAvatar from '@/components/common/AppAvatar.vue'
const { isDark } = useTheme() const { isDark } = useTheme()
const authStore = useAuthStore() const authStore = useAuthStore()
const toast = useToast()
const saving = ref(false) const saving = ref(false)
const field = ref('') const avatarUploading = ref(false)
const value = ref('')
const placeholder = ref('')
const maxLength = ref(0)
const pageTitle = ref('编辑资料')
const inputLabel = ref('')
function goBack() { uni.navigateBack() } /** 表单数据,与后端 User 字段对齐 */
const form = reactive({
onMounted(() => { name: '',
const pages = getCurrentPages() avatar: '',
const currentPage = pages[pages.length - 1] desc: '',
const options = (currentPage as any).$page?.options || {} region: ''
field.value = options.field || ''
value.value = decodeURIComponent(options.value || '')
switch (field.value) {
case 'name':
placeholder.value = '请输入昵称'
maxLength.value = 20
pageTitle.value = '修改昵称'
inputLabel.value = '昵称'
break
case 'desc':
placeholder.value = '请输入个性签名'
maxLength.value = 100
pageTitle.value = '修改签名'
inputLabel.value = '个性签名'
break
case 'region':
placeholder.value = '请输入地区'
maxLength.value = 50
pageTitle.value = '修改地区'
inputLabel.value = '地区'
break
}
}) })
/** 从 store 初始化表单 */
function initForm() {
const u = authStore.user
if (!u) return
form.name = u.name || ''
form.avatar = u.avatar || ''
form.desc = u.desc || ''
form.region = u.region || ''
}
function goBack() {
uni.navigateBack()
}
onMounted(() => {
initForm()
})
/** 选择并上传头像,复用群头像上传模式 */
function chooseAvatar() {
if (avatarUploading.value) return
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: async (res) => {
const filePath = res.tempFilePaths[0]
if (!filePath) return
avatarUploading.value = true
try {
toast.loading('上传中...')
const attachment = await attachmentApi.uploadAttachment(filePath, 'image')
form.avatar = attachment.file_url
toast.close()
toast.success('头像已更新')
} catch (e: any) {
toast.close()
toast.error(e.message || '头像上传失败')
} finally {
avatarUploading.value = false
}
}
})
}
/** 保存全部资料字段 */
async function save() { async function save() {
if (!value.value.trim()) { if (!form.name.trim()) {
uni.showToast({ title: '内容不能为空', icon: 'none' }) toast.error('昵称不能为空')
return return
} }
if (saving.value) return if (saving.value) return
saving.value = true
try {
const userId = authStore.user?.id const userId = authStore.user?.id
if (!userId) { if (!userId) {
uni.showToast({ title: '用户信息异常', icon: 'none' }) toast.error('用户信息异常')
return return
} }
saving.value = true
// 构建更新数据 try {
const updates: Record<string, string> = {} const updates = {
updates[field.value] = value.value.trim() name: form.name.trim(),
avatar: form.avatar,
// 调用 API 更新用户信息 desc: form.desc.trim(),
const updatedUser = await userApi.updateUser({ id: userId, updates }) region: form.region.trim()
// 更新本地 store
if (authStore.user) {
const newUserInfo = { ...authStore.user, ...updates }
authStore.updateUserInfo(newUserInfo)
} }
await userApi.updateUser({ id: userId, updates })
uni.showToast({ title: '保存成功', icon: 'success' }) const latest = await userApi.getMyInfo()
setTimeout(() => { uni.navigateBack() }, 1500) authStore.updateUserInfo(latest)
} catch (error: any) { toast.success('保存成功')
console.error('保存失败:', error) setTimeout(() => uni.navigateBack(), 1200)
uni.showToast({ title: error.message || '保存失败', icon: 'none' }) } catch (e: any) {
console.error('保存失败:', e)
toast.error(e.message || '保存失败')
} finally { } finally {
saving.value = false saving.value = false
} }
@@ -133,7 +201,6 @@ async function save() {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
// 页面容器 - 浅色模式
.edit-page { .edit-page {
--bg-page: #f7f8fa; --bg-page: #f7f8fa;
--bg-surface: #ffffff; --bg-surface: #ffffff;
@@ -141,12 +208,12 @@ async function save() {
--text-secondary: #6b7280; --text-secondary: #6b7280;
--text-tertiary: #9ca3af; --text-tertiary: #9ca3af;
--color-brand: #4F46E5; --color-brand: #4F46E5;
--border-color: rgba(0, 0, 0, 0.05);
min-height: 100vh; min-height: 100vh;
background: var(--bg-page); background: var(--bg-page);
} }
// 深色模式 - Warm Stone
.edit-page.dark { .edit-page.dark {
--bg-page: #1c1917; --bg-page: #1c1917;
--bg-surface: #292524; --bg-surface: #292524;
@@ -154,9 +221,9 @@ async function save() {
--text-secondary: #e7e5e4; --text-secondary: #e7e5e4;
--text-tertiary: #78716c; --text-tertiary: #78716c;
--color-brand: #f97316; --color-brand: #f97316;
--border-color: rgba(255, 255, 255, 0.08);
} }
// 动画
@keyframes fadeInUp { @keyframes fadeInUp {
from { from {
opacity: 0; opacity: 0;
@@ -173,7 +240,6 @@ async function save() {
opacity: 0; opacity: 0;
} }
// 导航栏
.nav-bar { .nav-bar {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -219,47 +285,106 @@ async function save() {
} }
} }
// 内容
.content-body { .content-body {
padding: 40rpx; padding: 40rpx;
} }
// 输入卡片 .avatar-section {
.input-card { display: flex;
background: var(--bg-surface); flex-direction: column;
border-radius: 24rpx; align-items: center;
padding: 32rpx;
position: relative;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.03);
margin-bottom: 48rpx; margin-bottom: 48rpx;
.input-label { .avatar-wrap {
font-size: 26rpx; position: relative;
font-weight: 600; width: 192rpx;
color: var(--text-secondary); height: 192rpx;
margin-bottom: 16rpx; border-radius: 50%;
overflow: visible;
.avatar-mask {
position: absolute;
inset: 0;
border-radius: 50%;
background: rgba(0, 0, 0, 0.4);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.2s;
svg {
width: 64rpx;
height: 64rpx;
} }
} }
.custom-input { &:active .avatar-mask {
width: 100%; opacity: 1;
font-size: 32rpx; }
.avatar-badge {
position: absolute;
bottom: 0;
right: 0;
width: 48rpx;
height: 48rpx;
background: var(--color-brand);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 4rpx solid var(--bg-page);
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
svg {
width: 24rpx;
height: 24rpx;
}
}
}
.avatar-hint {
font-size: 24rpx;
color: var(--text-tertiary);
margin-top: 24rpx;
}
}
.info-card {
background: var(--bg-surface);
border-radius: 24rpx;
overflow: hidden;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.03);
margin-bottom: 32rpx;
border: 1rpx solid var(--border-color);
}
.field-row {
display: flex;
align-items: center;
padding: 28rpx 32rpx;
border-bottom: 1rpx solid var(--border-color);
&.no-border {
border-bottom: none;
}
.field-label {
width: 112rpx;
font-size: 28rpx;
font-weight: 500;
color: var(--text-secondary);
flex-shrink: 0;
}
.field-input {
flex: 1;
font-size: 28rpx;
color: var(--text-primary); color: var(--text-primary);
background: transparent; background: transparent;
border: none; border: none;
outline: none; outline: none;
padding: 16rpx 0;
border-bottom: 2rpx solid transparent;
transition: border-color 0.2s;
&:focus {
border-bottom-color: var(--color-brand);
}
&.textarea {
height: 200rpx;
line-height: 1.6;
resize: none;
} }
} }
@@ -267,17 +392,45 @@ async function save() {
color: var(--text-tertiary); color: var(--text-tertiary);
} }
.counter { .desc-card {
position: absolute; background: var(--bg-surface);
bottom: 20rpx; border-radius: 24rpx;
right: 32rpx; padding: 32rpx;
font-size: 24rpx; box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.03);
color: var(--text-tertiary); margin-bottom: 48rpx;
border: 1rpx solid var(--border-color);
position: relative;
.desc-label {
font-size: 28rpx;
font-weight: 500;
color: var(--text-secondary);
margin-bottom: 16rpx;
display: block;
}
.desc-input {
width: 100%;
height: 192rpx;
font-size: 28rpx;
color: var(--text-primary);
line-height: 1.6;
background: transparent;
border: none;
outline: none;
}
.counter {
display: block;
text-align: right;
font-size: 24rpx;
color: var(--text-tertiary);
margin-top: 8rpx;
}
} }
// 保存按钮
.btn-wrap { .btn-wrap {
margin-top: 40rpx; margin-top: 16rpx;
} }
.save-btn { .save-btn {
@@ -299,8 +452,12 @@ async function save() {
box-shadow: 0 8rpx 24rpx rgba(249, 115, 22, 0.25); box-shadow: 0 8rpx 24rpx rgba(249, 115, 22, 0.25);
} }
&:active { &:active:not(.disabled) {
transform: scale(0.98); transform: scale(0.98);
} }
&.disabled {
opacity: 0.7;
}
} }
</style> </style>

View File

@@ -6,7 +6,20 @@
<view class="profile-header"> <view class="profile-header">
<view class="header-top"> <view class="header-top">
<text class="page-title">我的</text> <text class="page-title">我的</text>
<view class="icon-btn" @click="uni.navigateTo({ url: '/pages/settings/index' })"> <view class="header-actions">
<view class="edit-btn" @click="goProfileEdit">
<!-- #ifdef H5 || APP-PLUS -->
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
</svg>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<wd-icon name="edit" size="40rpx" />
<!-- #endif -->
<text class="edit-text">编辑资料</text>
</view>
<view class="icon-btn" @click="goSettings">
<!-- #ifdef H5 || APP-PLUS --> <!-- #ifdef H5 || APP-PLUS -->
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="3"/> <circle cx="12" cy="12" r="3"/>
@@ -18,23 +31,18 @@
<!-- #endif --> <!-- #endif -->
</view> </view>
</view> </view>
</view>
<view class="user-hero animate-fade-in-up" @click="goProfileEdit"> <!-- 用户信息展示区仅展示不可点击跳转 -->
<view class="user-hero animate-fade-in-up">
<view class="avatar-wrap"> <view class="avatar-wrap">
<app-avatar :src="userInfo.avatar" :name="userInfo.nickname" :size="160" round custom-style="border: 6rpx solid var(--bg-surface);" /> <app-avatar :src="user?.avatar" :name="user?.name" :size="160" round custom-style="border: 6rpx solid var(--bg-surface);" />
</view> </view>
<view class="hero-info"> <view class="hero-info">
<text class="nickname">{{ userInfo.nickname || '未设置昵称' }}</text> <text class="nickname">{{ user?.name || '未设置昵称' }}</text>
<text class="account">账号: {{ userInfo.account || '--' }}</text> <text class="account">账号: {{ displayAccount }}</text>
<text v-if="user?.desc" class="desc">{{ user.desc }}</text>
</view> </view>
<!-- #ifdef H5 || APP-PLUS -->
<svg class="chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="9 18 15 12 9 6"/>
</svg>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<wd-icon name="arrow-right" size="32rpx" custom-class="chevron-mp" />
<!-- #endif -->
</view> </view>
<!-- 数据概览 --> <!-- 数据概览 -->
@@ -128,26 +136,53 @@
</view> </view>
</view> </view>
<app-tab-bar current="profile" /> <!-- <app-tab-bar current="profile" />-->
</view> </view>
</wd-config-provider> </wd-config-provider>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { useTheme } from '@/composables/useTheme' import { useTheme } from '@/composables/useTheme'
import { useAuthStore } from '@/stores' import { useAuthStore } from '@/stores'
import * as userApi from '@/api/modules/user'
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'
const { isDark } = useTheme() const { isDark } = useTheme()
const authStore = useAuthStore() const authStore = useAuthStore()
const userInfo = computed(() => authStore.userInfo || {})
function goProfileEdit() { uni.navigateTo({ url: '/pages/profile/edit' }) } const user = computed(() => authStore.user)
/** 展示账号:优先手机号,其次邮箱 */
const displayAccount = computed(() => {
const u = user.value
if (!u) return '--'
return u.phone || u.email || '--'
})
/** 进入编辑页(仅通过编辑按钮触发) */
function goProfileEdit() {
uni.navigateTo({ url: '/pages/profile/edit' })
}
function goSettings() {
uni.navigateTo({ url: '/pages/settings/index' })
}
/** 每次显示时拉取最新用户信息 */
onShow(async () => {
try {
const info = await userApi.getMyInfo()
authStore.updateUserInfo(info)
} catch (e) {
console.error('刷新用户信息失败:', e)
}
})
</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;
@@ -162,7 +197,6 @@ function goProfileEdit() { uni.navigateTo({ url: '/pages/profile/edit' }) }
padding-bottom: 200rpx; padding-bottom: 200rpx;
} }
// 深色模式 - Warm Stone
.page-container.dark { .page-container.dark {
--bg-page: #1c1917; --bg-page: #1c1917;
--bg-surface: #292524; --bg-surface: #292524;
@@ -173,7 +207,6 @@ function goProfileEdit() { uni.navigateTo({ url: '/pages/profile/edit' }) }
--color-brand: #f97316; --color-brand: #f97316;
} }
// 动画
@keyframes fadeInUp { @keyframes fadeInUp {
from { from {
opacity: 0; opacity: 0;
@@ -190,7 +223,6 @@ function goProfileEdit() { uni.navigateTo({ url: '/pages/profile/edit' }) }
opacity: 0; opacity: 0;
} }
// 头部
.profile-header { .profile-header {
background: var(--bg-surface); background: var(--bg-surface);
border-bottom-left-radius: 80rpx; border-bottom-left-radius: 80rpx;
@@ -219,6 +251,39 @@ function goProfileEdit() { uni.navigateTo({ url: '/pages/profile/edit' }) }
letter-spacing: -0.5rpx; letter-spacing: -0.5rpx;
} }
.header-actions {
display: flex;
align-items: center;
gap: 16rpx;
}
.edit-btn {
display: flex;
align-items: center;
gap: 8rpx;
padding: 12rpx 20rpx;
border-radius: 32rpx;
background: rgba(79, 70, 229, 0.08);
transition: all 0.15s;
svg {
width: 32rpx;
height: 32rpx;
color: var(--color-brand);
}
.edit-text {
font-size: 24rpx;
font-weight: 500;
color: var(--color-brand);
}
&:active {
opacity: 0.8;
transform: scale(0.98);
}
}
.icon-btn { .icon-btn {
width: 72rpx; width: 72rpx;
height: 72rpx; height: 72rpx;
@@ -245,21 +310,18 @@ function goProfileEdit() { uni.navigateTo({ url: '/pages/profile/edit' }) }
display: flex; display: flex;
align-items: center; align-items: center;
gap: 24rpx; gap: 24rpx;
transition: all 0.15s;
&:active {
opacity: 0.8;
}
.avatar-wrap { .avatar-wrap {
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1); box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1);
border-radius: 50%; border-radius: 50%;
flex-shrink: 0;
} }
.hero-info { .hero-info {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-width: 0;
.nickname { .nickname {
font-size: 40rpx; font-size: 40rpx;
@@ -272,16 +334,18 @@ function goProfileEdit() { uni.navigateTo({ url: '/pages/profile/edit' }) }
font-size: 26rpx; font-size: 26rpx;
color: var(--text-secondary); color: var(--text-secondary);
} }
}
.chevron { .desc {
width: 32rpx; font-size: 24rpx;
height: 32rpx;
color: var(--text-tertiary); color: var(--text-tertiary);
margin-top: 8rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
} }
} }
// 数据统计
.stats-row { .stats-row {
display: flex; display: flex;
justify-content: space-around; justify-content: space-around;
@@ -308,7 +372,6 @@ function goProfileEdit() { uni.navigateTo({ url: '/pages/profile/edit' }) }
} }
} }
// 菜单
.menu-container { .menu-container {
padding: 40rpx; padding: 40rpx;
} }
@@ -344,17 +407,9 @@ function goProfileEdit() { uni.navigateTo({ url: '/pages/profile/edit' }) }
height: 40rpx; height: 40rpx;
} }
&.indigo { &.indigo { background: #4F46E5; }
background: #4F46E5; &.amber { background: #f59e0b; }
} &.emerald { background: #10b981; }
&.amber {
background: #f59e0b;
}
&.emerald {
background: #10b981;
}
} }
.menu-text { .menu-text {