feat(user): enable opening profile edit modal by clicking avatar or nickname

这个提交包含在:
xcbyy
2026-10-13 08:44:36 +08:00
父节点 8d85ed54b2
当前提交 6e78a7e4b7
共有 2 个文件被更改,包括 122 次插入 和 19 次删除

查看文件

@@ -2,11 +2,16 @@
<view class="user-container" :class="{ 'theme-warm': currentThemeKey === 'warm' }" :style="navStyle">
<!-- 用户头部个人资料卡 -->
<view class="profile-card" @click="!isLoggedIn && openLoginModal()">
<image class="user-avatar" :src="isLoggedIn && user.avatar ? user.avatar : DEFAULT_GUEST_AVATAR" mode="aspectFill" />
<image
class="user-avatar btn-active"
:src="isLoggedIn && user.avatar ? user.avatar : DEFAULT_GUEST_AVATAR"
mode="aspectFill"
@click.stop="handleAvatarOrNameClick"
/>
<view class="user-info">
<!-- 顶层水平对齐行:昵称/VIP 与 月亮太阳切换/设置 在同一水平线 -->
<view class="user-top-row">
<view class="name-row">
<view class="name-row btn-active" @click.stop="handleAvatarOrNameClick">
<text class="user-name">{{ isLoggedIn ? user.nickname : '点击登录 / 注册' }}</text>
<text class="vip-badge" v-if="isLoggedIn">VIP 黄金玩家</text>
<text class="guest-badge" v-else>未登录</text>
@@ -29,12 +34,12 @@
<image class="theme-icon-svg" :src="ICONS.sun" mode="aspectFit" />
</view>
</view>
<view class="edit-btn btn-active" v-if="isLoggedIn" @click.stop="editProfile">
<view class="edit-btn btn-active" v-if="isLoggedIn" @click.stop="editProfile" title="设置资料">
<image class="edit-svg" :src="ICONS.settings" mode="aspectFit" />
</view>
</view>
</view>
<text class="user-uid">{{ isLoggedIn ? `UID: ${user.id}` : '登录后享受完整陪伴服务' }}</text>
<text class="user-uid" @click.stop="handleAvatarOrNameClick">{{ isLoggedIn ? `UID: ${user.id}` : '登录后享受完整陪伴服务' }}</text>
</view>
</view>
@@ -164,13 +169,20 @@
</view>
<view class="modal-body">
<!-- 预设头像切换与真实上传 -->
<!-- 预设头像切换与真实上传 (支持微信官方头像与相册上传) -->
<view class="edit-avatar-section">
<image class="avatar-preview btn-active" :src="editForm.avatar" mode="aspectFill" @click="chooseCustomAvatar" />
<view class="avatar-meta">
<view class="avatar-label-row">
<text class="avatar-label">点击更换推荐头像</text>
<text class="btn-custom-upload btn-active" @click="chooseCustomAvatar">相册上传</text>
<view class="avatar-action-links">
<!-- #ifdef MP-WEIXIN -->
<button class="btn-wechat-choose btn-active" open-type="chooseAvatar" @chooseavatar="onWechatChooseAvatar">
微信头像
</button>
<!-- #endif -->
<text class="btn-custom-upload btn-active" @click="chooseCustomAvatar">相册上传</text>
</view>
</view>
<view class="preset-avatars-row">
<image
@@ -186,14 +198,15 @@
</view>
</view>
<!-- 昵称输入 -->
<!-- 昵称输入 (支持微信官方 type="nickname" 快速轻触填入) -->
<view class="edit-field">
<text class="field-label">用户昵称</text>
<input
class="field-input"
type="nickname"
v-model="editForm.nickname"
maxlength="15"
placeholder="请输入新昵称"
placeholder="请输入新昵称或轻触填入微信昵称"
placeholder-class="field-placeholder"
/>
</view>
@@ -520,6 +533,32 @@ const editProfile = () => {
showEditModal.value = true;
};
// 点击头像或名称行:已登录时直接打开个人资料设置弹窗,未登录时引导登录
const handleAvatarOrNameClick = () => {
if (isLoggedIn.value) {
editProfile();
} else {
openLoginModal();
}
};
// 微信官方快捷选取头像回调
const onWechatChooseAvatar = async (e) => {
const avatarUrl = e?.detail?.avatarUrl;
if (!avatarUrl) return;
uni.showLoading({ title: '正在同步微信头像...' });
try {
const onlineUrl = await uploadImage(avatarUrl);
editForm.value.avatar = onlineUrl || avatarUrl;
uni.hideLoading();
uni.showToast({ title: '微信头像获取成功', icon: 'success' });
} catch (err) {
uni.hideLoading();
editForm.value.avatar = avatarUrl;
uni.showToast({ title: '已选取微信头像', icon: 'none' });
}
};
const chooseCustomAvatar = () => {
uni.chooseImage({
count: 1,
@@ -873,6 +912,13 @@ const navTo = (url) => {
border: 4rpx solid #6366F1;
margin-right: 24rpx;
flex-shrink: 0;
cursor: pointer;
transition: transform 0.15s ease, box-shadow 0.15s ease;
&:active {
transform: scale(0.95);
box-shadow: 0 0 16rpx rgba(99, 102, 241, 0.4);
}
}
.user-info {
@@ -892,6 +938,12 @@ const navTo = (url) => {
gap: 12rpx;
min-width: 0;
flex: 1;
cursor: pointer;
transition: opacity 0.15s ease;
&:active {
opacity: 0.75;
}
.user-name {
font-size: 32rpx;
@@ -986,6 +1038,12 @@ const navTo = (url) => {
font-size: 22rpx;
color: #64748B;
font-family: monospace;
cursor: pointer;
display: inline-block;
&:active {
opacity: 0.7;
}
}
}
}
@@ -1328,15 +1386,40 @@ const navTo = (url) => {
display: block;
}
.btn-custom-upload {
font-size: 22rpx;
color: #6366F1;
font-weight: 600;
padding: 4rpx 14rpx;
background: rgba(99, 102, 241, 0.12);
border: 1px solid rgba(99, 102, 241, 0.3);
border-radius: 8rpx;
cursor: pointer;
.avatar-action-links {
display: flex;
align-items: center;
gap: 12rpx;
.btn-wechat-choose {
margin: 0;
padding: 4rpx 14rpx;
font-size: 22rpx;
color: #10B981;
font-weight: 600;
background: rgba(16, 185, 129, 0.12);
border: 1px solid rgba(16, 185, 129, 0.3);
border-radius: 8rpx;
line-height: 1.4;
cursor: pointer;
&::after {
display: none;
border: none;
}
}
.btn-custom-upload {
font-size: 22rpx;
color: #6366F1;
font-weight: 600;
padding: 4rpx 14rpx;
background: rgba(99, 102, 241, 0.12);
border: 1px solid rgba(99, 102, 241, 0.3);
border-radius: 8rpx;
cursor: pointer;
line-height: 1.4;
}
}
}
@@ -1551,8 +1634,17 @@ const navTo = (url) => {
}
.avatar-meta {
.avatar-label {
color: #78716C;
.avatar-action-links {
.btn-wechat-choose {
color: #059669;
background: #ECFDF5;
border-color: rgba(5, 150, 105, 0.3);
}
.btn-custom-upload {
color: #EA580C;
background: #FFF7ED;
border-color: rgba(234, 88, 12, 0.3);
}
}
.preset-avatars-row .preset-avatar-item.selected {

查看文件

@@ -397,6 +397,17 @@ describe('ContentSecurityService & Compliance Architecture (TDD 微信内容安
expect(content).toContain('if (!ensureLoggedIn()) return;');
});
it('个人中心支持轻触头像、名称与UID呼出个人信息设置弹窗,并支持微信官方选头像与快速填入昵称', () => {
const userIndexPath = path.join(rootDir, 'pwuniapp', 'pages', 'user', 'index.vue');
const content = fs.readFileSync(userIndexPath, 'utf-8');
expect(content).toContain('handleAvatarOrNameClick');
expect(content).toContain('showEditModal');
expect(content).toContain('editProfile');
expect(content).toContain('open-type="chooseAvatar"');
expect(content).toContain('type="nickname"');
expect(content).toContain('onWechatChooseAvatar');
});
it('所有个人中心改造代码严禁包含任何系统 Emoji', () => {
const userIndexPath = path.join(rootDir, 'pwuniapp', 'pages', 'user', 'index.vue');
const text = fs.readFileSync(userIndexPath, 'utf-8');