upload project

This commit is contained in:
unknown 2025-12-27 17:16:03 +08:00
commit 06961cae04
422 changed files with 110626 additions and 0 deletions

View file

@ -0,0 +1,310 @@
// 临时用户资料预览页(扫码非好友进入)
const friendAPI = require('../../../utils/friend-api.js');
const apiClient = require('../../../utils/api-client.js');
const app = getApp();
Page({
data:{
customId:'',
user:{},
loading:true,
error:'',
relationStatus:'',
showAddButton:false,
privacyBlocked:false,
blockedRelation:false,
statusAdd:0 // 0 不是好友 1 已发送添加好友申请 2 已好友
},
onLoad(query){
const customId = (query?.customId || '10703945')+"" ;
if(!customId){
this.setData({ error:'缺少用户ID', loading:false });
return;
}
this.setData({ customId });
this.loadUser(customId);
},
async loadUser(customId){
this.setData({ loading:true, error:'' });
try{
// 使用新的接口 /user/info/{customId} 获取用户信息
const response = await apiClient.getUserInfoByCustomId(customId);
if(!response || response.code !== 0 || !response.data){
this.setData({ error:'用户不存在', loading:false });
return;
}
const user = response.data;
console.log('✅ 获取用户信息成功:', user);
// 处理关系状态
let relationStatus = user.relationStatus || 'can_add';
if(!relationStatus){
relationStatus = 'can_add'; // 未提供时默认可添加
}
// 处理年龄:优先使用接口返回的 age如果没有则根据 birthday 计算
let calculatedAge = user.age;
if(!calculatedAge && user.birthday){
calculatedAge = this.calculateAge(user.birthday);
}
// 处理家乡地址:将数组转换为字符串
let hometownText = '';
if (user.hometown) {
if (Array.isArray(user.hometown)) {
hometownText = user.hometown.filter(item => item).join(' ');
} else if (typeof user.hometown === 'string') {
hometownText = user.hometown;
}
}
// 判断是否显示添加好友按钮和状态
// 根据 relationStatus 判断:
// - "self": 自己,不显示添加按钮
// - "friend": 已是好友statusAdd = 2
// - "pending": 已发送申请statusAdd = 1
// - 其他: 可添加statusAdd = 0
let showAddButton = false;
let statusAdd = 0;
if (relationStatus === 'self') {
// 自己,不显示添加按钮
showAddButton = false;
statusAdd = 0;
} else if (relationStatus === 'friend') {
// 已是好友
showAddButton = false;
statusAdd = 2;
} else if (relationStatus === 'pending' || relationStatus === 'requested') {
// 已发送申请
showAddButton = false;
statusAdd = 1;
} else {
// 可以添加好友
showAddButton = true;
statusAdd = 0;
}
console.log('👀 用户预览信息:', {
relationStatus,
showAddButton,
statusAdd,
calculatedAge,
hometownText
});
this.setData({
user: user, // 使用完整的用户数据对象
relationStatus,
showAddButton,
privacyBlocked: relationStatus === 'privacy_limited',
blockedRelation: relationStatus === 'blocked',
calculatedAge: calculatedAge,
hometownText: hometownText,
loading: false,
statusAdd: statusAdd
});
}catch(e){
console.error('❌ 加载用户失败:', e);
let errorMessage = '加载失败';
if(e.message){
errorMessage = e.message;
} else if(e.errMsg){
errorMessage = e.errMsg;
}
this.setData({ error: errorMessage, loading:false });
}
},
reload(){
if(this.data.customId){ this.loadUser(this.data.customId); }
},
async addFriend(){
if(!this.data.user.customId) return;
try{
const dialogRes = await this.showAddFriendDialog(this.data.user.nickname||this.data.user.customId);
if(!dialogRes.confirm) return;
wx.showLoading({ title:'发送中...' });
const frienRes = await friendAPI.addFriend(this.data.user.customId, dialogRes.message);
console.log('frienRes -------frienRes ',frienRes);
wx.hideLoading();
wx.showToast({ title:'请求已发送', icon:'success' });
this.setData({ relationStatus:'pending', showAddButton:false,statusAdd:1 });
}catch(e){
wx.hideLoading();
wx.showToast({ title:e.message||'发送失败', icon:'none' });
}
},
goChat(){
const { user } = this.data;
if(!user.customId) return;
wx.navigateTo({ url:`/pages/message/chat/chat?targetId=${user.customId}&name=${encodeURIComponent(user.nickname||'好友')}&chatType=0` });
},
previewImage(e) {
try {
// 获取当前点击的图片索引和动态索引
const avatar = e.currentTarget.dataset.avatar;
// 🔥 设置标志,防止预览关闭后触发页面刷新
this._skipNextOnShowReload = true;
const imageUrls=[avatar];
// 调用微信小程序的图片预览API
wx.previewImage({
current: avatar,
urls: imageUrls,
success: () => {
// 预览打开成功,标志已设置,关闭时会触发 onShow
},
fail: (err) => {
console.error('图片预览失败:', err);
// 预览失败,清除标志
this._skipNextOnShowReload = false;
wx.showToast({
title: '预览图片失败',
icon: 'none'
});
}
});
} catch (error) {
console.error('图片预览过程中出错:', error);
// 出错时清除标志
this._skipNextOnShowReload = false;
wx.showToast({
title: '预览图片失败',
icon: 'none'
});
}
},
// 发送消息(与 goChat 功能相同)
sendMessage() {
const { user } = this.data;
if (!user || !user.customId) {
wx.showToast({
title: '用户信息不存在',
icon: 'none'
});
return;
}
wx.navigateTo({
url: `/pages/message/chat/chat?targetId=${user.customId}&name=${encodeURIComponent(user.nickname || user.customId || '好友')}&chatType=0`
});
},
showAddFriendDialog(nickname){
return new Promise(resolve=>{
wx.showModal({
title:`添加 ${nickname} 为好友`,
editable:true,
placeholderText:'请输入验证消息',
content:`我是 ${app.globalData.userInfo?.user?.nickname||''}`,
success:(res)=>{
resolve({
confirm:res.confirm,
message:res.content || `我是 ${app.globalData.userInfo?.user?.nickname||''}`
});
},
fail:()=>resolve({ confirm:false })
});
});
},
// 根据生日计算年龄
calculateAge(birthday) {
if (!birthday) {
return null;
}
try {
// 处理 YYYY-MM-DD 格式的日期
let birthDate;
if (typeof birthday === 'string' && birthday.includes('-')) {
const dateParts = birthday.split('-');
if (dateParts.length === 3) {
birthDate = new Date(parseInt(dateParts[0]), parseInt(dateParts[1]) - 1, parseInt(dateParts[2]));
} else {
birthDate = new Date(birthday);
}
} else {
birthDate = new Date(birthday);
}
if (isNaN(birthDate.getTime())) {
return null;
}
const today = new Date();
let age = today.getFullYear() - birthDate.getFullYear();
const monthDiff = today.getMonth() - birthDate.getMonth();
// 如果还没过生日年龄减1
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
age--;
}
// 年龄应该大于等于0
return age >= 0 ? age : null;
} catch (error) {
console.error('计算年龄失败:', error, birthday);
return null;
}
},
// 复制ID
copyId() {
const { user } = this.data;
const id = user?.customId || (user?.id ? 'findme_' + user.id : '未设置');
if (!id || id === '未设置') {
wx.showToast({
title: 'ID不存在',
icon: 'none'
});
return;
}
wx.setClipboardData({
data: id,
success: () => {
wx.showToast({
title: '复制成功',
icon: 'success',
duration: 2000
});
},
fail: () => {
wx.showToast({
title: '复制失败',
icon: 'none'
});
}
});
},
// 跳转到二维码页面
navigateToQRCode() {
const { user } = this.data;
if (!user || !user.customId) {
wx.showToast({
title: '用户信息不存在',
icon: 'none'
});
return;
}
// 传递陌生人的 customId 参数
wx.navigateTo({
url: `/subpackages/qr/qr-code/qr-code?customId=${user.customId}`,
success: (res) => {
console.log('跳转二维码页面成功', res);
},
fail: (err) => {
console.error('跳转二维码页面失败', err);
wx.showToast({
title: '跳转失败',
icon: 'none'
});
}
});
}
});

View file

@ -0,0 +1,7 @@
{
"navigationBarTitleText": "陌生人",
"navigationBarBackgroundColor": "#000000",
"navigationBarTextStyle": "white",
"backgroundColor": "#121212",
"usingComponents": {}
}

View file

@ -0,0 +1,157 @@
<view class="friend-detail-container">
<view class="loading-container" wx:if="{{loading}}">
<view class="loading-spinner"></view>
<text class="loading-text">加载中...</text>
</view>
<view class="error-box" wx:elif="{{error}}">
<text class="error-text">{{error}}</text>
<button class="retry-btn" bindtap="reload">重试</button>
</view>
<scroll-view class="detail-content" scroll-y="true" wx:else>
<view class="info-out">
<view class="info-card">
<view class="basic-info">
<view class="profile-top">
<view class="avatar-container">
<image class="avatar-image" data-avatar="{{user.avatar}}" bindtap="previewImage" wx:if="{{user.avatar}}" src="{{user.avatar}}" mode="aspectFill" />
<view wx:else class="avatar-placeholder">
<text class="avatar-text">{{user.nickname ? user.nickname.charAt(0) : '?'}}</text>
</view>
<view class="online-status online"></view>
</view>
<view class="profile-info">
<view class="profile-name-row">
<view class="profile-name">{{user.nickname || user.customId || 'FindMe用户'}}</view>
<view class="vip-crown" wx:if="{{user.isMember}}">👑</view>
</view>
<view class="profile-id">
<view>
<text class="id-label">ID: </text>
<view class="id-value-wrapper" bindtap="copyId">
<text class="id-value">{{user.customId}}</text>
</view>
</view>
<view class="tag" wx:if="{{user.verified}}">
<image class="tagImg" src="/images/tag.png" mode=""/>
<text class="tagText">已认证</text>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
<view class="info-out">
<view class="profile-bottom">
<view class="action-buttons">
<!-- <view class="edit-btn" bindtap="goChat">
<image src="/images/StartGroupChat.svg" class="edit-icon"/>
<text class="edit-text">发消息</text>
</view> -->
<view class="qr-code-btn" bindtap="navigateToQRCode">
<image src="/images/qr-code.png" class="qr-code-icon"/>
</view>
</view>
<!-- 位置信息 -->
<view class="profile-location">
<image src="/images/location.png" class="location-icon"/>
<text class="location-text-qr">{{hometownText || '未设置'}}</text>
<text class="location-arrow"></text>
</view>
<view class="profile-signature">
<text class="signature-text">{{user.bio || '暂无简介'}}</text>
</view>
<view class="profile-tabs">
<scroll-view scroll-x="true" class="tab-scroll" enable-flex="true">
<view class="tab-item" wx:if="{{user.gender !== null && user.gender !== undefined && user.gender !== ''}}">
<image wx:if="{{user.gender === 1 || user.gender === '1' || user.gender === 2 || user.gender === '2'}}" class="gender-icon" src="{{user.gender === 1 || user.gender === '1' ? '/images/self/male.svg' : '/images/self/fmale.svg'}}" mode="aspectFit" />
<text wx:else></text>
</view>
<view class="tab-item" wx:if="{{(calculatedAge !== null && calculatedAge !== undefined) || (user.age !== null && user.age !== undefined)}}">
<text>{{calculatedAge !== null && calculatedAge !== undefined ? (calculatedAge + '岁') : (user.age ? (user.age + '岁') : '')}}</text>
</view>
<view class="tab-item" wx:if="{{user.mood && user.mood !== ''}}">
<text>{{user.mood}}</text>
</view>
<view class="tab-item" wx:if="{{user.mbtiType && user.mbtiType !== ''}}">
<text>{{user.mbtiType}}</text>
</view>
<view class="tab-item" wx:if="{{user.identity && user.identity !== ''}}">
<text>{{user.identity}}</text>
</view>
<view class="tab-item" wx:if="{{user.zodiacSign && user.zodiacSign !== ''}}">
<text>{{user.zodiacSign}}</text>
</view>
<view class="tab-item" wx:if="{{user.school && user.school !== ''}}">
<text>{{user.school}}</text>
</view>
<view class="tab-item" wx:if="{{user.occupation && user.occupation !== ''}}">
<text>{{user.occupation}}</text>
</view>
</scroll-view>
</view>
</view>
</view>
<!-- <view class="info-out">
<view class="myfootprint">
<view class="footprint-title">Dasha的足迹</view>
<image class="footprint-earth" src="/images/self/earth.png" mode="aspectFit"/>
<view class="footprint-badge"><text>暂无足迹</text></view>
</view>
</view> -->
<!-- 新增两个渐变圆角容器 -->
<!-- <view class="info-out">
<view class="footprint-gradient-row">
<view class="footprint-gradient-box footprint-gradient-yellow">
<image class="video" src="/images/self/icon-video.png" mode=""/>
</view>
<view class="footprint-gradient-box footprint-gradient-blue">
<image class="video" src="/images/self/icon-video.png" mode=""/>
</view>
</view>
</view> -->
<!-- 操作按钮区 -->
<view class="info-out but-bom">
<view class="detail-action-buttons">
<view class="detail-action-btn-wrapper" bindtap="sendMessage">
<view class="detail-action-btn">
<text>消息</text>
</view>
</view>
<view class="detail-action-btn-wrapper" bindtap="addFriend" wx:if="{{statusAdd === 0}}">
<view class="detail-action-btn">
<text>加为好友</text>
</view>
</view>
<view class="detail-action-btn-wrapper gray" wx:if="{{statusAdd === 1}}">
<view class="detail-action-btn">
<text>已发送申请</text>
</view>
</view>
<view class="detail-action-btn-wrapper gray" wx:if="{{statusAdd === 2}}">
<view class="detail-action-btn">
<text>已是好友</text>
</view>
</view>
</view>
</view>
</scroll-view>
</view>

View file

@ -0,0 +1,500 @@
.friend-detail-container {
min-height: 100vh;
background: radial-gradient(ellipse at right bottom, #1c3954 0%, #1d3c7c 40%, #193170 70%, rgba(25, 49, 112, 0.8) 100%);
}
.detail-content {
height: 100vh;
padding: 24rpx 28rpx 48rpx 28rpx;
padding-bottom: 120rpx;
box-sizing: border-box;
background: transparent;
}
.info-card {
margin: 32rpx;
}
.info-out {
/* border: 1px solid red; */
width: 90%;
margin: 0 auto;
overflow: visible;
}
.profile-top {
display: flex;
align-items: center;
}
.avatar-container {
width: 186rpx;
height: 186rpx;
position: relative;
background: #000;
border-radius: 50%;
border: 10rpx solid #394e83;
transition: all 0.3s ease;
z-index: 999;
}
.avatar-image {
width: 166rpx;
height: 166rpx;
object-fit: cover;
border-radius: 50%;
position: absolute;
}
.avatar-placeholder {
width: 166rpx;
height: 166rpx;
border-radius: 50%;
background: #2a2a2a;
display: flex;
align-items: center;
justify-content: center;
}
.avatar-text {
font-size: 64rpx;
color: #888;
font-weight: 600;
}
.online-status.online {
position: absolute;
top: 20rpx;
right: 5rpx;
width: 32rpx;
height: 32rpx;
background: #4CAF50;
border: 3rpx solid #ffffff;
border-radius: 50%;
}
.profile-info {
flex: 1;
padding-left: 24rpx;
color: #fff;
}
.profile-name-row {
display: flex;
align-items: center;
flex-direction: row;
}
.profile-name {
font-size: 36rpx;
font-weight: 500;
color: #ffffff;
}
.gender-badge {
font-size: 24rpx;
margin-left: 8rpx;
color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
}
.gender-badge .gender-icon {
width: 32rpx;
height: 32rpx;
}
.vip-crown {
font-size: 24rpx;
margin-left: 8rpx;
color: #ffd700;
}
.profile-id {
display: flex;
align-items: center;
width: fit-content;
}
.id-label {
font-size: 28rpx;
color: #f3f3f3;
margin-right: 12rpx;
}
.id-value-wrapper {
display: inline-flex;
align-items: center;
padding: 10rpx 8rpx;
margin-right: 12rpx;
border-radius: 8rpx;
transition: background-color 0.2s ease;
}
.id-value-wrapper:active {
background-color: rgba(255, 255, 255, 0.1);
}
.id-value {
font-size: 28rpx;
color: #ffffff;
font-weight: 400;
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
}
.profile-bio .bio-text {
font-size: 28rpx;
color: #b0b0b0;
}
.profile-bottom {
background: linear-gradient(123deg, #8361FB 15.54%, #70AAFC 39.58%, #F0F8FB 62.43%, #F07BFF 90.28%);
border-radius: 28rpx;
padding-top: 15rpx;
gap: 16rpx;
position: relative;
z-index: 0;
margin-top: -70rpx;
padding-bottom: 20rpx;
display: flex;
flex-direction: column;
overflow: visible;
}
.action-buttons {
display: flex;
width: fit-content;
margin-left: auto;
justify-content: flex-end;
gap: 4rpx;
padding-top: 20rpx;
position: absolute;
z-index: 3;
right: 0;
top: 0;
}
.qr-code-btn {
display: flex;
align-items: center;
gap: 12rpx;
transition: all 0.2s ease;
margin-top: 10rpx;
margin-right:20rpx;
}
.qr-code-icon,
.edit-icon {
width: 40rpx;
height: 40rpx;
}
.edit-text {
font-size: 24rpx;
color: #000;
}
.profile-location {
background: rgba(43, 43, 43, 1);
max-width: 80%;
width: fit-content;
display: flex;
align-items: center;
background-color: #36393e;
border-radius: 24rpx;
min-height: 48rpx;
padding: 0 20rpx;
margin-left: 32rpx;
margin-top: 40rpx;
overflow-x: auto;
overflow-y: hidden;
}
.location-icon {
width: 24rpx;
height: 24rpx;
margin-right: 12rpx;
}
.location-arrow {
font-size: 36rpx;
color: #ffffff;
margin-left: auto;
font-weight: 300;
}
.location-text-qr {
font-size: 30rpx;
color: #e0e0e0;
line-height: 1.4;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
margin-right: 20rpx;
}
.profile-signature {
min-height: 120rpx;
max-height: 300rpx;
display: flex;
align-items: flex-start;
padding: 16rpx 24rpx;
background: rgba(90, 90, 90, 0.548);
border-radius: 24rpx;
backdrop-filter: blur(20rpx);
-webkit-backdrop-filter: blur(20rpx);
border: 1rpx solid rgba(255, 255, 255, 0.2);
box-shadow: 0 8rpx 12rpx rgba(0, 0, 0, 0.1), inset 0 0 20rpx rgba(255, 255, 255, 0.1);
margin: 0 32rpx;
overflow-y: auto;
overflow-x: hidden;
}
.signature-text {
font-size: 30rpx;
color: #000000;
margin: auto;
font-weight: 400;
width: 100%;
word-wrap: break-word;
word-break: break-all;
white-space: pre-wrap;
}
.profile-tabs {
width: 100%;
margin-bottom: 0;
padding: 0 40rpx;
}
.tab-scroll {
display: flex;
flex-direction: row;
white-space: nowrap;
align-items: center;
padding: 8rpx 0;
}
.tab-item {
font-size: 24rpx;
margin-top: 20rpx; /* slightly smaller text */
line-height: 36rpx; /* controls the text vertical alignment */
height: 36rpx; /* explicit height for the box */
display: flex; /* ensures vertical centering */
align-items: center; /* centers text in the box */
color: white;
padding: 6rpx 20rpx;
border-radius: 20rpx;
background: rgba(90, 90, 90, 0.548);
transition: all 0.2s;
margin-left: .3rem;
margin-right: .3rem;
}
.gender-icon {
width: 32rpx;
height: 32rpx;
display: flex;
align-items: center;
justify-content: center;
}
.myfootprint {
margin: 30rpx 0;
padding: 20rpx;
border-radius: 20rpx;
background: radial-gradient(circle at right, #0F918B 36%, #8827FF 50%, #0F918B 64%);
position: relative;
height: 236rpx;
display: flex;
align-items: center;
justify-content: center;
}
.footprint-title {
position: absolute;
top: 20rpx;
left: 20rpx;
font-size: 32rpx;
font-weight: 400;
color: #fff;
}
.footprint-earth {
width: 560rpx;
height: 120rpx;
position: relative;
top: 61%;
left: 40%;
transform: translate(-50%, -50%);
}
.footprint-badge {
position: absolute;
bottom: 20rpx;
right: 20rpx;
background: #fff;
padding: 0 20rpx;
border-radius: 10rpx;
font-size: 24rpx;
height: 56rpx;
line-height: 56rpx;
}
/* 足迹下方两个容器并排平分整行 */
.footprint-gradient-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 32rpx;
width: 100%;
margin: 32rpx auto;
}
.footprint-gradient-row .footprint-gradient-box {
width: 100%;
height: 332rpx;
border-radius: 40rpx;
margin: 0;
display: flex;
justify-content: center;
align-items: center;
}
/* 足迹下方两个渐变圆角容器样式 */
.footprint-gradient-box {
width: 100%;
height: 332rpx;
border-radius: 40rpx;
margin: 0;
}
.footprint-gradient-yellow {
background:radial-gradient( circle at center, #fff600 ,rgba(255, 255, 255, 0.1) );
}
.footprint-gradient-blue {
background: linear-gradient(130deg, #139dff 1%,#3137ea 40%,#3137ea 60%, #3137ea 80%, #3bc493 100%);
}
.detail-section.danger-section {
background: linear-gradient(-15deg, #00D5FF 1.54%, #435CFF 39.58%, #EC42C8 62.43%, #FF6460 90.28%);
border-radius: 44rpx;
height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
border: none;
box-shadow: none;
}
.but-bom {
margin-top: 70rpx;
}
/* 详细信息上方操作按钮 */
.detail-action-buttons {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin: 32rpx 0;
padding: 0 30rpx;
gap: 30rpx;
}
.detail-action-btn-wrapper {
flex: 1;
height: 64rpx;
border-radius: 32rpx;
padding: 2rpx;
background: linear-gradient(180deg, #4e4e4e 0%, #404040 100%);
}
.detail-action-btn-wrapper.gray {
opacity: 0.6;
}
.detail-action-btn {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
border-radius: 30rpx;
background: linear-gradient(180deg, #000 0%, #232323 100%);
}
.detail-action-btn text {
font-size: 28rpx;
color: #ffffff;
font-weight: 400;
}
.danger-item {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
gap: 6px;
}
.danger-text {
font-size: 28rpx;
color: #ffffff;
font-weight: 600;
letter-spacing: 1rpx;
}
.tag {
margin-left: 20rpx;
width: 150rpx;
display: flex;
justify-content: space-around;
}
.tagImg {
width: 30rpx;
height: 30rpx;
}
.tagText {
font-size: 24rpx;
}
.video{
width: 96rpx;
height: 96rpx;
}
.title-home{
width: 32rpx;
height: 32rpx;
}
.title-return{
width: 24rpx;
height: 24rpx;
}
.title{
width: 400rpx;
display: flex;
justify-content: space-around;
align-items: center;
height: 50rpx;
/* border: 1px solid red; */
margin-left: 40rpx;
background-color: #36393E;
border-radius: 14rpx;
color: white;
}
.gray{
background-color: #494949;
}