upload project
This commit is contained in:
commit
06961cae04
422 changed files with 110626 additions and 0 deletions
903
subpackages/profile/avatar-edit/avatar-edit.js
Normal file
903
subpackages/profile/avatar-edit/avatar-edit.js
Normal file
|
|
@ -0,0 +1,903 @@
|
|||
// 头像编辑页面
|
||||
const cosManager = require('../../../utils/cos-manager.js');
|
||||
const apiClient = require('../../../utils/api-client.js');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
// 图片路径
|
||||
originalImagePath: '', // 原始图片路径
|
||||
currentImagePath: '', // 当前编辑的图片路径
|
||||
imageInfo: null, // 图片信息
|
||||
|
||||
// 编辑状态
|
||||
rotateAngle: 0, // 旋转角度
|
||||
hasEdits: false, // 是否有编辑操作
|
||||
editHistory: [], // 编辑历史(用于撤回)
|
||||
|
||||
// 裁剪相关
|
||||
isCropping: false, // 是否在裁剪模式
|
||||
cropSize: 300, // 裁剪框大小(圆形)
|
||||
cropX: 0, // 裁剪框X坐标
|
||||
cropY: 0, // 裁剪框Y坐标
|
||||
|
||||
// 模糊相关
|
||||
isBlurring: false, // 是否在模糊模式
|
||||
blurSize: 20, // 模糊块大小
|
||||
blurData: [], // 模糊数据
|
||||
|
||||
// Canvas相关
|
||||
canvasContext: null,
|
||||
canvas: null,
|
||||
canvasWidth: 0,
|
||||
canvasHeight: 0,
|
||||
|
||||
// 系统信息
|
||||
systemInfo: {}
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
const imagePath = options.imagePath;
|
||||
if (!imagePath) {
|
||||
wx.showToast({
|
||||
title: '图片路径无效',
|
||||
icon: 'none'
|
||||
});
|
||||
setTimeout(() => {
|
||||
wx.navigateBack();
|
||||
}, 1500);
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取系统信息
|
||||
const systemInfo = wx.getSystemInfoSync();
|
||||
this.setData({
|
||||
systemInfo,
|
||||
originalImagePath: decodeURIComponent(imagePath),
|
||||
currentImagePath: decodeURIComponent(imagePath)
|
||||
});
|
||||
},
|
||||
|
||||
// 加载图片信息(从image的bindload事件调用)
|
||||
onImageLoad(e) {
|
||||
const { width, height } = e.detail;
|
||||
const systemInfo = this.data.systemInfo || wx.getSystemInfoSync();
|
||||
|
||||
// 计算显示尺寸
|
||||
const maxWidth = systemInfo.windowWidth;
|
||||
const maxHeight = systemInfo.windowHeight * 0.7;
|
||||
const scale = Math.min(maxWidth / width, maxHeight / height);
|
||||
|
||||
const displayWidth = width * scale;
|
||||
const displayHeight = height * scale;
|
||||
|
||||
// 获取图片容器的实际位置,用于计算裁剪框位置
|
||||
const query = wx.createSelectorQuery().in(this);
|
||||
query.select('.image-wrapper').boundingClientRect((rect) => {
|
||||
if (rect) {
|
||||
// 缓存图片容器的位置信息
|
||||
this._imageWrapperRect = rect;
|
||||
|
||||
// 初始化裁剪框位置(居中,相对于图片容器)
|
||||
const cropSize = Math.min(rect.width, rect.height) * 0.8;
|
||||
// 计算相对于图片容器的坐标
|
||||
const cropX = Math.max(0, (rect.width - cropSize) / 2);
|
||||
const cropY = Math.max(0, (rect.height - cropSize) / 2);
|
||||
|
||||
this.setData({
|
||||
imageInfo: { width, height, displayWidth, displayHeight },
|
||||
cropSize: Math.round(cropSize),
|
||||
cropX: Math.round(cropX),
|
||||
cropY: Math.round(cropY),
|
||||
canvasWidth: Math.round(rect.width), // 使用容器的实际宽度
|
||||
canvasHeight: Math.round(rect.height), // 使用容器的实际高度
|
||||
systemInfo: systemInfo // 确保 systemInfo 已设置
|
||||
}, () => {
|
||||
this.initCanvas();
|
||||
});
|
||||
} else {
|
||||
// 如果获取容器位置失败,使用默认值
|
||||
const cropSize = Math.min(displayWidth, displayHeight) * 0.8;
|
||||
const cropX = Math.max(0, (displayWidth - cropSize) / 2);
|
||||
const cropY = Math.max(0, (displayHeight - cropSize) / 2);
|
||||
|
||||
// 使用估算的容器位置
|
||||
this._imageWrapperRect = {
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: displayWidth,
|
||||
height: displayHeight
|
||||
};
|
||||
|
||||
this.setData({
|
||||
imageInfo: { width, height, displayWidth, displayHeight },
|
||||
cropSize: Math.round(cropSize),
|
||||
cropX: Math.round(cropX),
|
||||
cropY: Math.round(cropY),
|
||||
canvasWidth: Math.round(displayWidth),
|
||||
canvasHeight: Math.round(displayHeight),
|
||||
systemInfo: systemInfo // 确保 systemInfo 已设置
|
||||
}, () => {
|
||||
this.initCanvas();
|
||||
});
|
||||
}
|
||||
}).exec();
|
||||
},
|
||||
|
||||
// 初始化Canvas
|
||||
initCanvas() {
|
||||
const query = wx.createSelectorQuery().in(this);
|
||||
query.select('#blurCanvas')
|
||||
.fields({ node: true, size: true })
|
||||
.exec((res) => {
|
||||
if (res && res[0] && res[0].node) {
|
||||
const canvas = res[0].node;
|
||||
const ctx = canvas.getContext('2d');
|
||||
const { canvasWidth, canvasHeight } = this.data;
|
||||
|
||||
canvas.width = canvasWidth;
|
||||
canvas.height = canvasHeight;
|
||||
|
||||
this.setData({
|
||||
canvas: canvas,
|
||||
canvasContext: ctx
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 旋转图片
|
||||
rotateImage() {
|
||||
const newAngle = (this.data.rotateAngle + 90) % 360;
|
||||
this.saveEditState();
|
||||
this.setData({
|
||||
rotateAngle: newAngle,
|
||||
hasEdits: true
|
||||
});
|
||||
},
|
||||
|
||||
// 切换裁剪模式
|
||||
toggleCrop() {
|
||||
this.setData({
|
||||
isCropping: !this.data.isCropping,
|
||||
isBlurring: false
|
||||
}, () => {
|
||||
// 如果开启裁剪模式,重新获取图片容器位置
|
||||
if (this.data.isCropping) {
|
||||
this._updateImageWrapperRect();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 裁剪框拖动开始
|
||||
onCropMoveStart(e) {
|
||||
if (!this.data.isCropping) return;
|
||||
|
||||
const touch = e.touches[0];
|
||||
if (!touch) return;
|
||||
|
||||
// 确保容器位置已获取
|
||||
if (!this._imageWrapperRect) {
|
||||
this._updateImageWrapperRect();
|
||||
}
|
||||
|
||||
// 保存起始触摸位置
|
||||
this._cropStartTouch = {
|
||||
clientX: touch.clientX,
|
||||
clientY: touch.clientY
|
||||
};
|
||||
},
|
||||
|
||||
// 裁剪框拖动(优化版本,使用相对坐标)
|
||||
onCropMove(e) {
|
||||
if (!this.data.isCropping) return;
|
||||
|
||||
const touch = e.touches[0];
|
||||
if (!touch || !touch.clientX || !touch.clientY) return;
|
||||
|
||||
const { cropSize, canvasWidth, canvasHeight } = this.data;
|
||||
|
||||
// 保存触摸坐标,避免异步回调中失效
|
||||
const touchX = touch.clientX;
|
||||
const touchY = touch.clientY;
|
||||
|
||||
// 获取图片容器的实际位置(使用缓存的或重新获取)
|
||||
let imageWrapperRect = this._imageWrapperRect;
|
||||
|
||||
if (!imageWrapperRect) {
|
||||
// 如果没有缓存的容器位置,同步获取(使用估算值作为兜底)
|
||||
const query = wx.createSelectorQuery().in(this);
|
||||
query.select('.image-wrapper').boundingClientRect((rect) => {
|
||||
if (rect) {
|
||||
this._imageWrapperRect = rect;
|
||||
// 使用保存的触摸坐标重新计算
|
||||
const savedTouch = { clientX: touchX, clientY: touchY };
|
||||
this._calculateCropPosition(savedTouch, cropSize, canvasWidth, canvasHeight, rect);
|
||||
} else {
|
||||
// 如果获取失败,使用系统信息估算(兜底方案)
|
||||
const systemInfo = this.data.systemInfo || wx.getSystemInfoSync();
|
||||
const estimatedRect = {
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: systemInfo.windowWidth || 375,
|
||||
height: systemInfo.windowHeight * 0.7 || 400
|
||||
};
|
||||
const savedTouch = { clientX: touchX, clientY: touchY };
|
||||
this._calculateCropPosition(savedTouch, cropSize, canvasWidth, canvasHeight, estimatedRect);
|
||||
}
|
||||
}).exec();
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用缓存的容器位置计算裁剪框位置
|
||||
this._calculateCropPosition(touch, cropSize, canvasWidth, canvasHeight, imageWrapperRect);
|
||||
},
|
||||
|
||||
// 计算裁剪框位置(提取公共逻辑)
|
||||
_calculateCropPosition(touch, cropSize, canvasWidth, canvasHeight, imageWrapperRect) {
|
||||
// 确保 touch 对象有 clientX 和 clientY 属性
|
||||
if (!touch || typeof touch.clientX === 'undefined' || typeof touch.clientY === 'undefined') {
|
||||
console.warn('触摸事件坐标无效:', touch);
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算相对于图片容器的坐标
|
||||
const relativeX = touch.clientX - imageWrapperRect.left;
|
||||
const relativeY = touch.clientY - imageWrapperRect.top;
|
||||
|
||||
// 计算裁剪框中心位置(相对于图片容器)
|
||||
let newX = relativeX - cropSize / 2;
|
||||
let newY = relativeY - cropSize / 2;
|
||||
|
||||
// 限制裁剪框在图片范围内(确保左右和上下都能正常拖动)
|
||||
newX = Math.max(0, Math.min(newX, Math.max(0, canvasWidth - cropSize)));
|
||||
newY = Math.max(0, Math.min(newY, Math.max(0, canvasHeight - cropSize)));
|
||||
|
||||
// 确保数值类型正确
|
||||
this.setData({
|
||||
cropX: Math.round(newX),
|
||||
cropY: Math.round(newY)
|
||||
});
|
||||
},
|
||||
|
||||
// 更新图片容器位置(提取公共方法)
|
||||
_updateImageWrapperRect() {
|
||||
const query = wx.createSelectorQuery().in(this);
|
||||
query.select('.image-wrapper').boundingClientRect((rect) => {
|
||||
if (rect) {
|
||||
this._imageWrapperRect = rect;
|
||||
// 更新 canvasWidth 和 canvasHeight
|
||||
this.setData({
|
||||
canvasWidth: rect.width,
|
||||
canvasHeight: rect.height
|
||||
});
|
||||
}
|
||||
}).exec();
|
||||
},
|
||||
|
||||
// 裁剪框拖动结束
|
||||
onCropMoveEnd(e) {
|
||||
// 可以在这里添加拖动结束的逻辑,比如保存位置等
|
||||
// 目前不需要特殊处理
|
||||
},
|
||||
|
||||
// 切换模糊模式
|
||||
toggleBlur() {
|
||||
this.setData({
|
||||
isBlurring: !this.data.isBlurring,
|
||||
isCropping: false
|
||||
});
|
||||
},
|
||||
|
||||
// 模糊触摸处理
|
||||
onBlurTouch(e) {
|
||||
if (!this.data.isBlurring || !this.data.canvasContext) return;
|
||||
|
||||
const touch = e.touches[0];
|
||||
const { clientX, clientY } = touch;
|
||||
const { blurSize, canvasContext } = this.data;
|
||||
|
||||
// 绘制模糊块
|
||||
canvasContext.fillStyle = 'rgba(128, 128, 128, 0.6)';
|
||||
canvasContext.beginPath();
|
||||
canvasContext.arc(clientX, clientY, blurSize / 2, 0, 2 * Math.PI);
|
||||
canvasContext.fill();
|
||||
canvasContext.draw();
|
||||
|
||||
// 保存模糊数据
|
||||
const blurData = this.data.blurData || [];
|
||||
blurData.push({ x: clientX, y: clientY, size: blurSize });
|
||||
this.saveEditState();
|
||||
this.setData({
|
||||
blurData: blurData,
|
||||
hasEdits: true
|
||||
});
|
||||
},
|
||||
|
||||
// 保存编辑状态(用于撤回)
|
||||
saveEditState() {
|
||||
const history = this.data.editHistory || [];
|
||||
history.push({
|
||||
imagePath: this.data.currentImagePath,
|
||||
rotateAngle: this.data.rotateAngle,
|
||||
blurData: JSON.parse(JSON.stringify(this.data.blurData || []))
|
||||
});
|
||||
|
||||
// 限制历史记录数量
|
||||
if (history.length > 10) {
|
||||
history.shift();
|
||||
}
|
||||
|
||||
this.setData({
|
||||
editHistory: history
|
||||
});
|
||||
},
|
||||
|
||||
// 撤回操作
|
||||
undoEdit() {
|
||||
const history = this.data.editHistory || [];
|
||||
if (history.length === 0) {
|
||||
wx.showToast({
|
||||
title: '没有可撤回的操作',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const lastState = history.pop();
|
||||
this.setData({
|
||||
currentImagePath: lastState.imagePath,
|
||||
rotateAngle: lastState.rotateAngle,
|
||||
blurData: lastState.blurData,
|
||||
editHistory: history,
|
||||
hasEdits: history.length > 0
|
||||
});
|
||||
|
||||
// 重新渲染模糊
|
||||
if (this.data.canvasContext && lastState.blurData.length > 0) {
|
||||
this.reRenderBlur();
|
||||
}
|
||||
},
|
||||
|
||||
// 重新渲染模糊
|
||||
reRenderBlur() {
|
||||
const { canvasContext, blurData, canvasWidth, canvasHeight } = this.data;
|
||||
if (!canvasContext) return;
|
||||
|
||||
canvasContext.clearRect(0, 0, canvasWidth, canvasHeight);
|
||||
blurData.forEach(point => {
|
||||
canvasContext.fillStyle = 'rgba(128, 128, 128, 0.6)';
|
||||
canvasContext.beginPath();
|
||||
canvasContext.arc(point.x, point.y, point.size / 2, 0, 2 * Math.PI);
|
||||
canvasContext.fill();
|
||||
});
|
||||
canvasContext.draw();
|
||||
},
|
||||
|
||||
// 应用裁剪
|
||||
async applyCrop() {
|
||||
if (!this.data.isCropping) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
wx.showLoading({ title: '裁剪中...' });
|
||||
|
||||
try {
|
||||
const { currentImagePath, cropX, cropY, cropSize, imageInfo, rotateAngle } = this.data;
|
||||
|
||||
// 验证必要数据
|
||||
if (!currentImagePath) {
|
||||
throw new Error('图片路径无效');
|
||||
}
|
||||
|
||||
if (!imageInfo || !imageInfo.width || !imageInfo.height) {
|
||||
throw new Error('图片信息不完整');
|
||||
}
|
||||
|
||||
if (!cropX || !cropY || !cropSize || cropSize <= 0) {
|
||||
throw new Error('裁剪参数无效');
|
||||
}
|
||||
|
||||
// 创建Canvas进行裁剪(使用 Promise 包装 exec)
|
||||
const canvasRes = await new Promise((resolve, reject) => {
|
||||
const query = wx.createSelectorQuery().in(this);
|
||||
query.select('#cropCanvas')
|
||||
.fields({ node: true, size: true })
|
||||
.exec((res) => {
|
||||
if (!res || !res[0] || !res[0].node) {
|
||||
reject(new Error('Canvas初始化失败'));
|
||||
} else {
|
||||
resolve(res[0]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const canvas = canvasRes.node;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// 设置Canvas尺寸为裁剪大小
|
||||
const roundedCropSize = Math.round(cropSize);
|
||||
canvas.width = roundedCropSize;
|
||||
canvas.height = roundedCropSize;
|
||||
|
||||
// 加载图片(添加超时处理)
|
||||
const img = canvas.createImage();
|
||||
const imageLoadPromise = new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('图片加载超时'));
|
||||
}, 10000); // 10秒超时
|
||||
|
||||
img.onload = () => {
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
};
|
||||
img.onerror = (err) => {
|
||||
clearTimeout(timeout);
|
||||
reject(new Error('图片加载失败: ' + (err.message || '未知错误')));
|
||||
};
|
||||
});
|
||||
|
||||
img.src = currentImagePath;
|
||||
await imageLoadPromise;
|
||||
|
||||
// 计算裁剪区域在原图中的比例
|
||||
const scaleX = imageInfo.width / imageInfo.displayWidth;
|
||||
const scaleY = imageInfo.height / imageInfo.displayHeight;
|
||||
|
||||
const sourceX = Math.max(0, Math.round(cropX * scaleX));
|
||||
const sourceY = Math.max(0, Math.round(cropY * scaleY));
|
||||
const sourceSize = Math.min(
|
||||
Math.round(cropSize * scaleX),
|
||||
imageInfo.width - sourceX,
|
||||
imageInfo.height - sourceY
|
||||
);
|
||||
|
||||
// 清空画布
|
||||
ctx.clearRect(0, 0, roundedCropSize, roundedCropSize);
|
||||
|
||||
// 绘制圆形裁剪
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.arc(roundedCropSize / 2, roundedCropSize / 2, roundedCropSize / 2, 0, 2 * Math.PI);
|
||||
ctx.clip();
|
||||
|
||||
// 应用旋转
|
||||
if (rotateAngle !== 0) {
|
||||
ctx.translate(roundedCropSize / 2, roundedCropSize / 2);
|
||||
ctx.rotate(rotateAngle * Math.PI / 180);
|
||||
ctx.translate(-roundedCropSize / 2, -roundedCropSize / 2);
|
||||
}
|
||||
|
||||
// 绘制裁剪后的图片
|
||||
ctx.drawImage(img, sourceX, sourceY, sourceSize, sourceSize, 0, 0, roundedCropSize, roundedCropSize);
|
||||
ctx.restore();
|
||||
|
||||
// 导出图片
|
||||
const exportResult = await new Promise((resolve, reject) => {
|
||||
wx.canvasToTempFilePath({
|
||||
canvas: canvas,
|
||||
success: resolve,
|
||||
fail: reject
|
||||
}, this);
|
||||
});
|
||||
|
||||
if (!exportResult || !exportResult.tempFilePath) {
|
||||
throw new Error('生成裁剪图片失败');
|
||||
}
|
||||
|
||||
// 验证生成的文件
|
||||
const fileInfo = await new Promise((resolve, reject) => {
|
||||
wx.getFileInfo({
|
||||
filePath: exportResult.tempFilePath,
|
||||
success: resolve,
|
||||
fail: reject
|
||||
});
|
||||
});
|
||||
|
||||
if (!fileInfo || !fileInfo.size || fileInfo.size === 0) {
|
||||
throw new Error('裁剪后的文件无效');
|
||||
}
|
||||
|
||||
// 更新状态
|
||||
this.saveEditState();
|
||||
this.setData({
|
||||
currentImagePath: exportResult.tempFilePath,
|
||||
isCropping: false,
|
||||
hasEdits: true
|
||||
});
|
||||
|
||||
wx.hideLoading();
|
||||
console.log('裁剪成功,新图片路径:', exportResult.tempFilePath);
|
||||
|
||||
} catch (error) {
|
||||
wx.hideLoading();
|
||||
console.error('裁剪失败:', error);
|
||||
wx.showToast({
|
||||
title: error.message || '裁剪失败,请重试',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
throw error; // 重新抛出错误,让调用者处理
|
||||
}
|
||||
},
|
||||
|
||||
// 完成编辑并上传
|
||||
async confirmEdit() {
|
||||
wx.showLoading({ title: '处理中...' });
|
||||
|
||||
try {
|
||||
let finalImagePath = this.data.currentImagePath;
|
||||
|
||||
// 如果有裁剪,先应用裁剪
|
||||
if (this.data.isCropping) {
|
||||
try {
|
||||
await this.applyCrop();
|
||||
finalImagePath = this.data.currentImagePath;
|
||||
|
||||
// 验证裁剪后的文件路径
|
||||
if (!finalImagePath) {
|
||||
throw new Error('裁剪后未获取到图片路径');
|
||||
}
|
||||
|
||||
// 再次验证文件是否存在
|
||||
const fileInfo = await new Promise((resolve, reject) => {
|
||||
wx.getFileInfo({
|
||||
filePath: finalImagePath,
|
||||
success: resolve,
|
||||
fail: reject
|
||||
});
|
||||
});
|
||||
|
||||
if (!fileInfo || !fileInfo.size || fileInfo.size === 0) {
|
||||
throw new Error('裁剪后的文件无效,请重试');
|
||||
}
|
||||
|
||||
console.log('裁剪完成,文件路径:', finalImagePath, '文件大小:', fileInfo.size);
|
||||
} catch (cropError) {
|
||||
wx.hideLoading();
|
||||
console.error('裁剪失败:', cropError);
|
||||
wx.showToast({
|
||||
title: cropError.message || '裁剪失败,请重试',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
return; // 裁剪失败,直接返回,不继续上传
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有旋转或模糊,需要合并处理
|
||||
if (this.data.rotateAngle !== 0 || (this.data.blurData && this.data.blurData.length > 0)) {
|
||||
finalImagePath = await this.mergeEdits();
|
||||
}
|
||||
|
||||
// 验证文件路径
|
||||
if (!finalImagePath) {
|
||||
throw new Error('图片路径无效,请重新选择图片');
|
||||
}
|
||||
|
||||
// 检查文件是否存在
|
||||
try {
|
||||
const fileInfo = await new Promise((resolve, reject) => {
|
||||
wx.getFileInfo({
|
||||
filePath: finalImagePath,
|
||||
success: resolve,
|
||||
fail: reject
|
||||
});
|
||||
});
|
||||
console.log('文件信息:', fileInfo);
|
||||
} catch (fileError) {
|
||||
console.error('文件不存在或无法访问:', fileError);
|
||||
throw new Error('图片文件不存在或已损坏,请重新选择');
|
||||
}
|
||||
|
||||
// 上传头像
|
||||
console.log('开始上传头像,文件路径:', finalImagePath);
|
||||
|
||||
// 更新加载提示
|
||||
wx.showLoading({ title: '上传中...', mask: true });
|
||||
|
||||
// 添加上传超时处理(60秒)
|
||||
const uploadPromise = cosManager.upload(finalImagePath, {
|
||||
fileType: 'avatar',
|
||||
enableDedup: true,
|
||||
enableCompress: false,
|
||||
compressOptions: {
|
||||
maxWidth: 400,
|
||||
maxHeight: 400,
|
||||
targetSize: 30 * 1024
|
||||
},
|
||||
onProgress: (percent) => {
|
||||
wx.showLoading({
|
||||
title: `上传中 ${Math.round(percent)}%`,
|
||||
mask: true
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(new Error('上传超时,请检查网络后重试'));
|
||||
}, 60000); // 60秒超时
|
||||
});
|
||||
|
||||
const uploadResult = await Promise.race([uploadPromise, timeoutPromise]);
|
||||
|
||||
console.log('上传结果:', uploadResult);
|
||||
|
||||
// 检查上传结果
|
||||
if (!uploadResult) {
|
||||
throw new Error('上传失败:未获取到上传结果');
|
||||
}
|
||||
|
||||
// 检查 success 字段
|
||||
if (uploadResult.success !== true) {
|
||||
const errorMsg = uploadResult?.error || uploadResult?.message || '上传失败';
|
||||
console.error('上传失败:', errorMsg, uploadResult);
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
|
||||
// 检查 fileUrl 字段
|
||||
const avatarUrl = uploadResult.fileUrl || uploadResult.originalUrl;
|
||||
if (!avatarUrl) {
|
||||
console.error('上传结果中没有 fileUrl:', uploadResult);
|
||||
throw new Error('上传失败:未获取到文件URL');
|
||||
}
|
||||
|
||||
console.log('头像上传成功,URL:', avatarUrl);
|
||||
|
||||
// 更新用户信息
|
||||
await apiClient.updateUserProfile({ avatar: avatarUrl });
|
||||
|
||||
// 更新全局用户信息
|
||||
const app = getApp();
|
||||
if (app.globalData.userInfo && app.globalData.userInfo.user) {
|
||||
app.globalData.userInfo.user.avatar = avatarUrl;
|
||||
app.globalData.userInfo.user.avatarUrl = avatarUrl;
|
||||
}
|
||||
|
||||
// 同步到 NIM
|
||||
try {
|
||||
const nimUserManager = require('../../../utils/nim-user-manager.js');
|
||||
await nimUserManager.updateSelfUserInfo({
|
||||
avatar: avatarUrl
|
||||
});
|
||||
console.log('✅ 头像已同步到 NIM');
|
||||
} catch (nimError) {
|
||||
console.error('⚠️ 同步头像到 NIM 失败:', nimError);
|
||||
}
|
||||
|
||||
// 缓存头像
|
||||
let cachedUrl = avatarUrl;
|
||||
try {
|
||||
const imageCacheManager = require('../../../utils/image-cache-manager.js');
|
||||
cachedUrl = await imageCacheManager.cacheImage(avatarUrl, 'avatar');
|
||||
console.log('✅ 头像已缓存:', cachedUrl);
|
||||
} catch (cacheError) {
|
||||
console.error('⚠️ 头像缓存失败:', cacheError);
|
||||
}
|
||||
|
||||
wx.hideLoading();
|
||||
wx.showToast({
|
||||
title: '头像更新成功',
|
||||
icon: 'success'
|
||||
});
|
||||
|
||||
// 通知上一页更新头像
|
||||
const pages = getCurrentPages();
|
||||
console.log('当前页面栈:', pages.map(p => p.route));
|
||||
|
||||
// 查找 profile 页面
|
||||
let profilePage = null;
|
||||
for (let i = pages.length - 2; i >= 0; i--) {
|
||||
if (pages[i].route === 'subpackages/profile/profile/profile') {
|
||||
profilePage = pages[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (profilePage) {
|
||||
// 如果找到 profile 页面,更新头像
|
||||
if (typeof profilePage.onAvatarUpdated === 'function') {
|
||||
profilePage.onAvatarUpdated({
|
||||
avatarUrl: avatarUrl,
|
||||
cachedUrl: cachedUrl,
|
||||
serverUrl: avatarUrl,
|
||||
userInfo: {
|
||||
user: {
|
||||
...app.globalData.userInfo?.user,
|
||||
avatar: avatarUrl,
|
||||
avatarUrl: avatarUrl
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 如果上一页有 syncGlobalUserInfo 方法,也调用它
|
||||
if (typeof profilePage.syncGlobalUserInfo === 'function') {
|
||||
profilePage.syncGlobalUserInfo(cachedUrl || avatarUrl);
|
||||
}
|
||||
|
||||
// 如果上一页有 loadUserData 方法,调用它刷新数据
|
||||
if (typeof profilePage.loadUserData === 'function') {
|
||||
profilePage.loadUserData();
|
||||
}
|
||||
} else {
|
||||
// 如果找不到 profile 页面,尝试从上一页更新
|
||||
const prevPage = pages[pages.length - 2];
|
||||
if (prevPage) {
|
||||
if (typeof prevPage.onAvatarUpdated === 'function') {
|
||||
prevPage.onAvatarUpdated({
|
||||
avatarUrl: avatarUrl,
|
||||
cachedUrl: cachedUrl,
|
||||
serverUrl: avatarUrl,
|
||||
userInfo: {
|
||||
user: {
|
||||
...app.globalData.userInfo?.user,
|
||||
avatar: avatarUrl,
|
||||
avatarUrl: avatarUrl
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof prevPage.syncGlobalUserInfo === 'function') {
|
||||
prevPage.syncGlobalUserInfo(cachedUrl || avatarUrl);
|
||||
}
|
||||
|
||||
if (typeof prevPage.loadUserData === 'function') {
|
||||
prevPage.loadUserData();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 返回上一页(确保返回到 profile 页面)
|
||||
setTimeout(() => {
|
||||
// 检查页面栈,确保返回到正确的页面
|
||||
const currentPages = getCurrentPages();
|
||||
console.log('返回前的页面栈:', currentPages.map(p => p.route));
|
||||
|
||||
// 检查上一页是否是 profile 页面
|
||||
if (currentPages.length >= 2) {
|
||||
const prevPageRoute = currentPages[currentPages.length - 2].route;
|
||||
console.log('上一页路由:', prevPageRoute);
|
||||
|
||||
// 如果上一页是 profile 页面,直接返回
|
||||
if (prevPageRoute === 'subpackages/profile/profile/profile') {
|
||||
wx.navigateBack({
|
||||
delta: 1,
|
||||
success: () => {
|
||||
console.log('成功返回到 profile 页面');
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('返回失败:', err);
|
||||
// 如果返回失败,直接跳转到 profile 页面
|
||||
wx.redirectTo({
|
||||
url: '/subpackages/profile/profile/profile',
|
||||
success: () => {
|
||||
console.log('使用 redirectTo 成功跳转到 profile 页面');
|
||||
},
|
||||
fail: (redirectErr) => {
|
||||
console.error('redirectTo 也失败:', redirectErr);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 如果上一页不是 profile 页面,直接跳转到 profile 页面
|
||||
console.log('上一页不是 profile 页面,直接跳转到 profile');
|
||||
wx.redirectTo({
|
||||
url: '/subpackages/profile/profile/profile',
|
||||
success: () => {
|
||||
console.log('成功跳转到 profile 页面');
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('跳转到 profile 失败:', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 如果页面栈中只有当前页面,直接跳转到 profile
|
||||
console.log('页面栈中只有当前页面,直接跳转到 profile');
|
||||
wx.redirectTo({
|
||||
url: '/subpackages/profile/profile/profile',
|
||||
success: () => {
|
||||
console.log('成功跳转到 profile 页面');
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('跳转到 profile 失败:', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
} catch (error) {
|
||||
wx.hideLoading();
|
||||
console.error('上传失败,详细错误:', error);
|
||||
console.error('错误堆栈:', error.stack);
|
||||
|
||||
// 提供更详细的错误信息
|
||||
let errorMessage = '上传失败';
|
||||
if (error.message) {
|
||||
errorMessage = error.message;
|
||||
} else if (typeof error === 'string') {
|
||||
errorMessage = error;
|
||||
} else if (error.errMsg) {
|
||||
errorMessage = error.errMsg;
|
||||
}
|
||||
|
||||
wx.showToast({
|
||||
title: errorMessage,
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 合并所有编辑效果
|
||||
async mergeEdits() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const query = wx.createSelectorQuery().in(this);
|
||||
query.select('#mergeCanvas')
|
||||
.fields({ node: true, size: true })
|
||||
.exec((res) => {
|
||||
if (!res || !res[0] || !res[0].node) {
|
||||
reject(new Error('Canvas初始化失败'));
|
||||
return;
|
||||
}
|
||||
|
||||
const canvas = res[0].node;
|
||||
const ctx = canvas.getContext('2d');
|
||||
const { imageInfo, rotateAngle, blurData } = this.data;
|
||||
|
||||
// 设置Canvas尺寸
|
||||
canvas.width = imageInfo.width;
|
||||
canvas.height = imageInfo.height;
|
||||
|
||||
// 加载图片
|
||||
const img = canvas.createImage();
|
||||
img.src = this.data.currentImagePath;
|
||||
|
||||
img.onload = () => {
|
||||
// 应用旋转
|
||||
ctx.save();
|
||||
ctx.translate(imageInfo.width / 2, imageInfo.height / 2);
|
||||
ctx.rotate(rotateAngle * Math.PI / 180);
|
||||
ctx.translate(-imageInfo.width / 2, -imageInfo.height / 2);
|
||||
|
||||
// 绘制图片
|
||||
ctx.drawImage(img, 0, 0, imageInfo.width, imageInfo.height);
|
||||
ctx.restore();
|
||||
|
||||
// 应用模糊(如果有)
|
||||
if (blurData && blurData.length > 0) {
|
||||
const scaleX = imageInfo.width / imageInfo.displayWidth;
|
||||
const scaleY = imageInfo.height / imageInfo.displayHeight;
|
||||
|
||||
blurData.forEach(point => {
|
||||
ctx.fillStyle = 'rgba(128, 128, 128, 0.6)';
|
||||
ctx.beginPath();
|
||||
ctx.arc(point.x * scaleX, point.y * scaleY, point.size * scaleX / 2, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
});
|
||||
}
|
||||
|
||||
// 导出图片
|
||||
wx.canvasToTempFilePath({
|
||||
canvas: canvas,
|
||||
success: (res) => {
|
||||
resolve(res.tempFilePath);
|
||||
},
|
||||
fail: reject
|
||||
}, this);
|
||||
};
|
||||
|
||||
img.onerror = reject;
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// 取消编辑
|
||||
cancelEdit() {
|
||||
wx.navigateBack();
|
||||
}
|
||||
});
|
||||
|
||||
9
subpackages/profile/avatar-edit/avatar-edit.json
Normal file
9
subpackages/profile/avatar-edit/avatar-edit.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"navigationBarTitleText": "编辑头像",
|
||||
"navigationBarBackgroundColor": "#000000",
|
||||
"navigationBarTextStyle": "white",
|
||||
"backgroundColor": "#000000"
|
||||
}
|
||||
|
||||
|
||||
|
||||
79
subpackages/profile/avatar-edit/avatar-edit.wxml
Normal file
79
subpackages/profile/avatar-edit/avatar-edit.wxml
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
<view class="avatar-edit-container">
|
||||
<!-- 图片编辑区域 -->
|
||||
<view class="edit-area">
|
||||
<view class="image-wrapper" style="transform: rotate({{rotateAngle}}deg);">
|
||||
<image
|
||||
class="edit-image"
|
||||
src="{{currentImagePath}}"
|
||||
mode="widthFix"
|
||||
bindload="onImageLoad"
|
||||
></image>
|
||||
|
||||
<!-- 裁剪框(圆形) -->
|
||||
<view
|
||||
wx:if="{{isCropping}}"
|
||||
class="crop-box"
|
||||
style="left: {{cropX}}px; top: {{cropY}}px; width: {{cropSize}}px; height: {{cropSize}}px;"
|
||||
catchtouchstart="onCropMoveStart"
|
||||
catchtouchmove="onCropMove"
|
||||
catchtouchend="onCropMoveEnd"
|
||||
>
|
||||
<view class="crop-circle"></view>
|
||||
<view class="crop-handle"></view>
|
||||
</view>
|
||||
|
||||
<!-- 模糊Canvas -->
|
||||
<canvas
|
||||
wx:if="{{isBlurring}}"
|
||||
id="blurCanvas"
|
||||
type="2d"
|
||||
class="blur-canvas"
|
||||
bindtouchstart="onBlurTouch"
|
||||
bindtouchmove="onBlurTouch"
|
||||
></canvas>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 工具按钮栏 -->
|
||||
<view class="toolbar">
|
||||
<view class="tool-item {{isCropping ? 'active' : ''}}" bindtap="toggleCrop">
|
||||
<text class="tool-icon">✂️</text>
|
||||
<text class="tool-text">裁剪</text>
|
||||
</view>
|
||||
|
||||
<view class="tool-item" bindtap="rotateImage">
|
||||
<text class="tool-icon">🔄</text>
|
||||
<text class="tool-text">旋转</text>
|
||||
</view>
|
||||
|
||||
<view class="tool-item {{isBlurring ? 'active' : ''}}" bindtap="toggleBlur">
|
||||
<text class="tool-icon">🌫️</text>
|
||||
<text class="tool-text">模糊</text>
|
||||
</view>
|
||||
|
||||
<view class="tool-item {{editHistory.length > 0 ? '' : 'disabled'}}" bindtap="undoEdit">
|
||||
<text class="tool-icon">↩️</text>
|
||||
<text class="tool-text">撤回</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<view class="bottom-actions">
|
||||
<button class="cancel-btn" bindtap="cancelEdit">取消</button>
|
||||
<button class="confirm-btn" bindtap="confirmEdit">完成</button>
|
||||
</view>
|
||||
|
||||
<!-- 隐藏的Canvas用于处理 -->
|
||||
<canvas
|
||||
id="cropCanvas"
|
||||
type="2d"
|
||||
style="position: fixed; left: -9999px; top: -9999px;"
|
||||
></canvas>
|
||||
|
||||
<canvas
|
||||
id="mergeCanvas"
|
||||
type="2d"
|
||||
style="position: fixed; left: -9999px; top: -9999px;"
|
||||
></canvas>
|
||||
</view>
|
||||
|
||||
135
subpackages/profile/avatar-edit/avatar-edit.wxss
Normal file
135
subpackages/profile/avatar-edit/avatar-edit.wxss
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
.avatar-edit-container {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background: #000000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.edit-area {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.image-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.edit-image {
|
||||
max-width: 100%;
|
||||
max-height: 70vh;
|
||||
}
|
||||
|
||||
/* 裁剪框样式 */
|
||||
.crop-box {
|
||||
position: absolute;
|
||||
border: 2px solid #ffffff;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.crop-circle {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
border: 2px dashed rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.crop-handle {
|
||||
position: absolute;
|
||||
right: -10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: #ffffff;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* 模糊Canvas */
|
||||
.blur-canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
/* 工具栏 */
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
background: #1a1a1a;
|
||||
border-top: 1px solid #333333;
|
||||
}
|
||||
|
||||
.tool-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20rpx;
|
||||
min-width: 120rpx;
|
||||
}
|
||||
|
||||
.tool-item.active {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 10rpx;
|
||||
}
|
||||
|
||||
.tool-item.disabled {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.tool-icon {
|
||||
font-size: 48rpx;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.tool-text {
|
||||
font-size: 24rpx;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* 底部操作栏 */
|
||||
.bottom-actions {
|
||||
display: flex;
|
||||
padding: 30rpx;
|
||||
background: #1a1a1a;
|
||||
border-top: 1px solid #333333;
|
||||
}
|
||||
|
||||
.cancel-btn,
|
||||
.confirm-btn {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
border-radius: 40rpx;
|
||||
font-size: 32rpx;
|
||||
margin: 0 15rpx;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
background: #333333;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.confirm-btn {
|
||||
background: linear-gradient(135deg, #044db4 0%, #156301 100%);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
|
||||
|
||||
658
subpackages/profile/personal-details/personal-details.js
Normal file
658
subpackages/profile/personal-details/personal-details.js
Normal file
|
|
@ -0,0 +1,658 @@
|
|||
const apiClient = require('../../../utils/api-client.js');
|
||||
const nimUserManager = require('../../../utils/nim-user-manager.js');
|
||||
const { calculateConstellation, calculateConstellationFromBirthday } = require('../../../utils/formatDate.js');
|
||||
const cosManager = require('../../../utils/cos-manager.js');
|
||||
|
||||
const occupationOptions = [
|
||||
"初中生", "高中生", "大学生", "研究生", "留学生", "科研", "警察", "医生", "护士",
|
||||
"程序员", "老师", "化妆师", "摄影师", "音乐", "美术", "金融", "厨师", "工程师",
|
||||
"公务员", "互联网", "产品经理", "模特", "演员", "导演", "律师", "创业者", "其他"
|
||||
];
|
||||
const schoolOptions = ["博士", "硕士", "本科", "专科", "中专", "高中", "初中", "小学", "其他"];
|
||||
const genderOptions = ["未知", "男", "女", "其他"];
|
||||
const genderMap = {
|
||||
"未知": 0,
|
||||
"男": 1,
|
||||
"女": 2,
|
||||
"其他": 3
|
||||
};
|
||||
// 反向映射:数字转文字
|
||||
const genderReverseMap = {
|
||||
0: "未知",
|
||||
1: "男",
|
||||
2: "女",
|
||||
3: "其他"
|
||||
};
|
||||
const zodiacSignOptions = ["双鱼座", "白羊座", "金牛座", "双子座", "巨蟹座", "狮子座", "处女座", "天秤座", "天蝎座", "射手座", "摩羯座", "水瓶座"];
|
||||
const mbtiTypeOptions = ["INTJ", "INTP", "ENTJ", "INFP", "ENTP", "INFJ", "ENFP", "ENFJ", "ISTJ", "ISFJ", "ISTP", "ISFP", "ESTJ", "ESFJ", "ESTP", "ESFP"];
|
||||
const sleepHabitOptions = ["早起鸟儿", "夜猫子", "规律型", "深度睡眠追求者", "碎片化睡眠者", "失眠困扰者", "咖啡因敏感型", "数字戒断者", "运动调节型", "挑战打卡型", "鼾声监测者", "生物钟调节者", "社区分享型"];
|
||||
const socialActivityOptions = ["内容创作者", "观察者", "吃瓜者", "潜水者", "机器人", "社群型用户", "KOL", "KOC", "普通用户", "算法依赖型用户", "事件驱动型用户", "季节性活跃用户", "社交维系型用户", "兴趣社群型用户", "职业网络型用户", "娱乐消遣型用户", "购物种草型用户", "互动型用户"];
|
||||
const heightOptions = Array.from({ length: 71 }, (_, i) => (140 + i));
|
||||
|
||||
Page({
|
||||
data: {
|
||||
occupationOptions,
|
||||
schoolOptions,
|
||||
genderOptions,
|
||||
zodiacSignOptions,
|
||||
mbtiTypeOptions,
|
||||
sleepHabitOptions,
|
||||
socialActivityOptions,
|
||||
heightOptions,
|
||||
|
||||
user: {
|
||||
nickname: "",
|
||||
avatar: "",
|
||||
bio: "",
|
||||
gender: 0, // 0=未知 1=男 2=女 3=其他
|
||||
birthday: "",
|
||||
ethnicity: "",
|
||||
occupation: "",
|
||||
school: "",
|
||||
hometown: [],
|
||||
zodiacSign: "",
|
||||
height: 0,
|
||||
mbtiType: "",
|
||||
sleepHabit: "",
|
||||
socialActivity: ""
|
||||
},
|
||||
|
||||
// 性别显示文字(用于 WXML 显示)
|
||||
genderDisplayText: "请选择",
|
||||
|
||||
// sheet 控制
|
||||
showSheet: false,
|
||||
sheetOptions: [],
|
||||
sheetTitle: "",
|
||||
sheetKey: "",
|
||||
selectedValue: "",
|
||||
|
||||
// 下拉刷新
|
||||
refreshing: false
|
||||
},
|
||||
|
||||
getKeyByValue(obj, value) {
|
||||
return Object.keys(obj).find(key => obj[key] === value);
|
||||
},
|
||||
|
||||
// 转换用户数据格式:从旧格式转换为新格式
|
||||
transformUserData(data) {
|
||||
if (!data) return this.clearObjectValues(this.data.user);
|
||||
|
||||
const transformed = {
|
||||
nickname: data.nickname || "",
|
||||
avatar: data.avatar || "",
|
||||
bio: data.bio || "",
|
||||
gender: typeof data.gender === 'number' ? data.gender : (genderMap[data.gender] !== undefined ? genderMap[data.gender] : 0),
|
||||
birthday: data.birthday || "",
|
||||
ethnicity: data.ethnicity || "",
|
||||
occupation: data.occupation || "",
|
||||
school: data.school || "", // 兼容旧字段 education
|
||||
hometown: Array.isArray(data.hometown) ? data.hometown : [],
|
||||
zodiacSign: data.zodiacSign || "", // 兼容旧字段 constellation
|
||||
height: typeof data.height === 'number' ? data.height : (data.height ? parseInt(data.height) || 0 : 0),
|
||||
mbtiType: data.mbtiType || "", // 兼容旧字段 personality
|
||||
sleepHabit: data.sleepHabit || "", // 兼容旧字段 sleep
|
||||
socialActivity: data.socialActivity || "" // 兼容旧字段 social
|
||||
};
|
||||
|
||||
// 如果有生日但没有星座,根据生日自动生成星座
|
||||
if (transformed.birthday && !transformed.zodiacSign) {
|
||||
const generatedZodiacSign = calculateConstellationFromBirthday(transformed.birthday);
|
||||
if (generatedZodiacSign) {
|
||||
transformed.zodiacSign = generatedZodiacSign;
|
||||
console.log('根据生日自动生成星座:', transformed.birthday, '->', generatedZodiacSign);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新性别显示文字
|
||||
this.updateGenderDisplay(transformed.gender);
|
||||
|
||||
return transformed;
|
||||
},
|
||||
|
||||
// 更新性别显示文字
|
||||
updateGenderDisplay(gender) {
|
||||
let displayText = "请选择";
|
||||
if (gender !== undefined && gender !== null) {
|
||||
if (typeof gender === 'number' && genderReverseMap[gender] !== undefined) {
|
||||
displayText = genderReverseMap[gender];
|
||||
} else if (typeof gender === 'string') {
|
||||
displayText = gender;
|
||||
}
|
||||
}
|
||||
this.setData({ genderDisplayText: displayText });
|
||||
},
|
||||
|
||||
async onLoad() {
|
||||
try {
|
||||
// 获取 app 实例
|
||||
const app = getApp();
|
||||
// 初始化屏幕适配
|
||||
await app.initScreen(this);
|
||||
|
||||
const pages = getCurrentPages();
|
||||
const currPage = pages[pages.length - 1]; // 当前页面实例
|
||||
const profile = pages[pages.length - 2]; // 上一个页面实例(profile)
|
||||
if (profile && profile.initSystemInfo) {
|
||||
profile.initSystemInfo();
|
||||
}
|
||||
if (profile && profile.loadUserData) {
|
||||
profile.loadUserData();
|
||||
}
|
||||
|
||||
const clearData = this.clearObjectValues(this.data.user);
|
||||
|
||||
// 获取用户信息
|
||||
try {
|
||||
const appData = await apiClient.getUserInfo();
|
||||
if (appData && appData.code === 0 && appData.data) {
|
||||
// 转换数据格式以匹配新的数据结构
|
||||
const userData = this.transformUserData(appData.data);
|
||||
this.setData({ user: userData });
|
||||
// app.globalData.userInfo = userData;
|
||||
// wx.setStorageSync('userInfo', userData);
|
||||
} else {
|
||||
// 如果获取失败,使用默认数据
|
||||
this.setData({ user: clearData });
|
||||
this.updateGenderDisplay(clearData.gender);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error);
|
||||
// 使用默认数据
|
||||
this.setData({ user: clearData });
|
||||
this.updateGenderDisplay(clearData.gender);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('页面加载失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
onUnload() {
|
||||
// 移除自动保存功能,改为点击保存按钮才保存
|
||||
// 用户可以通过保存按钮主动保存,或者直接返回(不保存)
|
||||
},
|
||||
|
||||
clearObjectValues(obj) {
|
||||
Object.keys(obj).forEach(key => {
|
||||
const value = obj[key];
|
||||
if (Array.isArray(value)) {
|
||||
// 清空为新数组
|
||||
obj[key] = [];
|
||||
} else {
|
||||
const map = {
|
||||
'object': {},
|
||||
'string': '',
|
||||
'number': 0,
|
||||
'boolean': false,
|
||||
}
|
||||
const type = typeof value;
|
||||
obj[key] = map[type] ?? null;
|
||||
}
|
||||
});
|
||||
return obj
|
||||
},
|
||||
|
||||
/* ---------- 头像 -------------- */
|
||||
async chooseAvatar() {
|
||||
try {
|
||||
// 选择图片
|
||||
const res = await new Promise((resolve, reject) => {
|
||||
wx.chooseImage({
|
||||
count: 1,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: resolve,
|
||||
fail: reject
|
||||
});
|
||||
});
|
||||
|
||||
if (!res.tempFilePaths || res.tempFilePaths.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filePath = res.tempFilePaths[0];
|
||||
|
||||
// 显示上传中提示
|
||||
wx.showLoading({ title: '上传中...', mask: true });
|
||||
|
||||
// 使用 COS 上传头像
|
||||
// const uploadResult = await cosManager.upload(filePath, {
|
||||
// fileType: 'avatar',
|
||||
// enableDedup: false, // 启用去重,节省存储空间
|
||||
// onProgress: (percent) => {
|
||||
// console.log('上传进度:', percent + '%');
|
||||
// }
|
||||
// });
|
||||
|
||||
// 使用 COS 上传头像
|
||||
const uploadResult = await cosManager.upload(filePath, {
|
||||
fileType: 'avatar',
|
||||
enableDedup: true, // 启用去重,节省存储空间
|
||||
onProgress: (percent) => {
|
||||
console.log('上传进度:', percent + '%');
|
||||
},
|
||||
enableCompress: false,
|
||||
compressOptions: {
|
||||
maxWidth: 400,
|
||||
maxHeight: 400,
|
||||
targetSize: 30 * 1024
|
||||
}
|
||||
});
|
||||
|
||||
console.log('上传结果:', uploadResult);
|
||||
|
||||
// 检查上传结果
|
||||
if (!uploadResult) {
|
||||
throw new Error('上传失败:未获取到上传结果');
|
||||
}
|
||||
|
||||
if (uploadResult.success !== true) {
|
||||
const errorMsg = uploadResult?.error || uploadResult?.message || '上传失败';
|
||||
console.error('上传失败:', errorMsg, uploadResult);
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
|
||||
// 获取上传后的文件URL
|
||||
const avatarUrl = uploadResult.fileUrl || uploadResult.originalUrl;
|
||||
|
||||
if (!avatarUrl) {
|
||||
console.error('上传返回数据:', uploadResult);
|
||||
throw new Error('上传失败:未获取到文件URL');
|
||||
}
|
||||
|
||||
|
||||
// 更新本地头像
|
||||
const user = Object.assign({}, this.data.user, { avatar: avatarUrl });
|
||||
this.setData({ user });
|
||||
|
||||
|
||||
|
||||
// app.globalData.userInfo.user= {
|
||||
// ...app.globalData.userInfo.user,
|
||||
// avatar:avatarUrl
|
||||
// };
|
||||
|
||||
|
||||
// 立即同步头像到 NIM
|
||||
try {
|
||||
await nimUserManager.updateSelfUserInfo({
|
||||
avatar: avatarUrl
|
||||
});
|
||||
console.log('✅ 头像已同步到 NIM');
|
||||
} catch (nimError) {
|
||||
console.error('⚠️ 同步头像到 NIM 失败:', nimError);
|
||||
// 不影响主流程,静默失败
|
||||
}
|
||||
|
||||
// 立即同步头像到业务后端
|
||||
try {
|
||||
await apiClient.updateUserProfile({ avatar: avatarUrl });
|
||||
console.log('✅ 头像已同步到后端');
|
||||
} catch (apiError) {
|
||||
console.error('⚠️ 同步头像到后端失败:', apiError);
|
||||
// 不影响主流程,静默失败
|
||||
}
|
||||
|
||||
// 通知 profile 页面更新头像
|
||||
try {
|
||||
const pages = getCurrentPages();
|
||||
const profilePage = pages[pages.length - 2];
|
||||
if (profilePage) {
|
||||
profilePage.onLoad();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('通知 profile 页面更新头像失败:', error);
|
||||
// 不影响主流程,静默失败
|
||||
}
|
||||
|
||||
wx.hideLoading();
|
||||
wx.showToast({
|
||||
title: '上传成功',
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
wx.hideLoading();
|
||||
console.error('上传头像失败:', error);
|
||||
wx.showToast({
|
||||
title: error.message || '上传失败,请重试',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// chooseImage(useCamera) {
|
||||
// const self = this;
|
||||
// wx.chooseImage({
|
||||
// count: 1,
|
||||
// sizeType: ['compressed'],
|
||||
// sourceType: useCamera ? ['camera'] : ['album'],
|
||||
// success(res) {
|
||||
// const tempPath = res.tempFilePaths[0];
|
||||
// const user = Object.assign({}, self.data.user, { avatar: tempPath });
|
||||
// self.setData({ user });
|
||||
// wx.setStorageSync('user_profile_v1', user);
|
||||
// }
|
||||
// });
|
||||
// },
|
||||
|
||||
// 昵称输入
|
||||
onNicknameInput(e) {
|
||||
const user = Object.assign({}, this.data.user, { nickname: e.detail.value });
|
||||
this.setData({ user });
|
||||
},
|
||||
|
||||
/* ---------- 个人简介 ---------- */
|
||||
onIntroInput(e) {
|
||||
// 直接更新 user.bio
|
||||
const user = Object.assign({}, this.data.user, { bio: e.detail.value });
|
||||
this.setData({ user });
|
||||
},
|
||||
|
||||
/* ---------- 种族 ---------- */
|
||||
onEthnicityInput(e) {
|
||||
const user = Object.assign({}, this.data.user, { ethnicity: e.detail.value });
|
||||
this.setData({ user });
|
||||
},
|
||||
|
||||
/* ---------- 毕业院校 ---------- */
|
||||
onSchoolInput(e) {
|
||||
const user = Object.assign({}, this.data.user, { school: e.detail.value });
|
||||
this.setData({ user });
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
/* ---------- bottom sheet 通用打开 ---------- */
|
||||
openSheet(e) {
|
||||
// data-options 会以字符串形式传入,如果直接传对象需要自定义组件
|
||||
// 这里我们支持两种写法:如果元素上有 data-options 属性且为 JSON 字符串,则尝试解析(注意:wxml 中直接传数组会被字符串化)
|
||||
const dataset = e.currentTarget.dataset || {};
|
||||
const key = dataset.key;
|
||||
const title = dataset.title || '请选择';
|
||||
let options = dataset.options;
|
||||
|
||||
// dataset.options 在 wxml 传递数组时通常会变成字符串 "[object Object]" —— 我们优先根据 key 在定义好的数组中取
|
||||
if (!options || typeof options === 'string') {
|
||||
const map = {
|
||||
occupation: occupationOptions,
|
||||
school: schoolOptions,
|
||||
gender: genderOptions,
|
||||
zodiacSign: zodiacSignOptions,
|
||||
mbtiType: mbtiTypeOptions,
|
||||
sleepHabit: sleepHabitOptions,
|
||||
socialActivity: socialActivityOptions,
|
||||
height: heightOptions,
|
||||
}
|
||||
options = map[key] || [];
|
||||
}
|
||||
// 获取当前选中值,如果是性别需要转换为文字显示
|
||||
let selectedValue = "";
|
||||
if (key === 'gender') {
|
||||
const genderNum = this.data.user[key];
|
||||
if (typeof genderNum === 'number' && genderReverseMap[genderNum] !== undefined) {
|
||||
selectedValue = genderReverseMap[genderNum];
|
||||
} else if (typeof genderNum === 'string') {
|
||||
selectedValue = genderNum;
|
||||
}
|
||||
} else {
|
||||
selectedValue = this.data.user[key] || "";
|
||||
}
|
||||
|
||||
this.setData({
|
||||
showSheet: true,
|
||||
sheetOptions: options,
|
||||
sheetTitle: title,
|
||||
sheetKey: key,
|
||||
selectedValue
|
||||
});
|
||||
},
|
||||
|
||||
closeSheet() {
|
||||
this.setData({ showSheet: false, sheetOptions: [], sheetTitle: "", sheetKey: "", selectedValue: "" });
|
||||
},
|
||||
|
||||
onSheetSelect(e) {
|
||||
const idx = e.currentTarget.dataset.index;
|
||||
const val = this.data.sheetOptions[idx];
|
||||
const key = this.data.sheetKey;
|
||||
if (!key) return this.closeSheet();
|
||||
|
||||
const user = Object.assign({}, this.data.user);
|
||||
// 如果是性别选择,需要转换为数字
|
||||
if (key === 'gender') {
|
||||
user[key] = genderMap[val] !== undefined ? genderMap[val] : 0;
|
||||
// 更新性别显示文字
|
||||
this.updateGenderDisplay(user[key]);
|
||||
} else {
|
||||
user[key] = val;
|
||||
}
|
||||
this.setData({ user, selectedValue: val }, () => {
|
||||
this.closeSheet();
|
||||
});
|
||||
},
|
||||
|
||||
/* ---------- 生日 ---------- */
|
||||
onBirthdayChange(e) {
|
||||
const val = e.detail.value;
|
||||
const user = Object.assign({}, this.data.user, { birthday: val });
|
||||
// 更新星座(使用公共方法)
|
||||
user.zodiacSign = calculateConstellationFromBirthday(val);
|
||||
this.setData({ user });
|
||||
},
|
||||
|
||||
/* ---------- 家乡 ---------- */
|
||||
onHometownChange(e) {
|
||||
const nick = e.detail?.value ?? this.data.user.hometown;
|
||||
const user = Object.assign({}, this.data.user, { hometown: nick });
|
||||
this.setData({ user });
|
||||
// 更新用户信息
|
||||
// apiClient.updateUserProfile(user)
|
||||
// .then(res => {
|
||||
// console.log('更新用户信息成功', res);
|
||||
// wx.showToast({ title: '更新用户信息成功', icon: 'success' });
|
||||
// this.setData({ user });
|
||||
// })
|
||||
// .catch(err => {
|
||||
// console.error('更新用户信息失败', err);
|
||||
// wx.showToast({ title: '更新用户信息失败', icon: 'none' });
|
||||
// });
|
||||
},
|
||||
|
||||
/* ---------- 下拉刷新 ---------- */
|
||||
onRefresh() {
|
||||
this.setData({ refreshing: true });
|
||||
// 重新加载用户信息
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const clearData = this.clearObjectValues(this.data.user);
|
||||
const appData = await apiClient.getUserInfo();
|
||||
if (appData && appData.code === 0 && appData.data) {
|
||||
// 转换数据格式以匹配新的数据结构
|
||||
const userData = this.transformUserData(appData.data);
|
||||
this.setData({ user: userData });
|
||||
} else {
|
||||
this.setData({ user: clearData });
|
||||
this.updateGenderDisplay(clearData.gender);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('刷新用户信息失败:', error);
|
||||
} finally {
|
||||
this.setData({ refreshing: false });
|
||||
}
|
||||
}, 500);
|
||||
},
|
||||
|
||||
/* ---------- 保存按钮 ---------- */
|
||||
async handleSave() {
|
||||
try {
|
||||
|
||||
// 延迟一下,确保失焦完成
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
wx.showLoading({ title: '保存中...', mask: true });
|
||||
|
||||
// 更新用户信息
|
||||
const user = Object.assign({}, this.data.user);
|
||||
// 确保性别是数字格式(0=未知 1=男 2=女 3=其他)
|
||||
if (typeof user.gender === 'string') {
|
||||
user.gender = genderMap[user.gender] !== undefined ? genderMap[user.gender] : 0;
|
||||
} else if (typeof user.gender !== 'number') {
|
||||
user.gender = 0;
|
||||
}
|
||||
// 确保身高是数字
|
||||
if (typeof user.height === 'string') {
|
||||
user.height = parseInt(user.height) || 0;
|
||||
}
|
||||
if (user.height === 0) {//==0提交会报错 所以删除
|
||||
delete user.height;
|
||||
}
|
||||
|
||||
const res = await apiClient.updateUserProfile(user);
|
||||
|
||||
// 同步昵称和头像到 NIM
|
||||
if (user.nickname) {
|
||||
try {
|
||||
await nimUserManager.updateSelfUserInfo({
|
||||
nickname: user.nickname,
|
||||
avatar: user.avatar || undefined
|
||||
});
|
||||
console.log('✅ 用户资料已同步到 NIM');
|
||||
} catch (nimError) {
|
||||
console.error('⚠️ 同步用户资料到 NIM 失败:', nimError);
|
||||
// 不影响主流程,静默失败
|
||||
}
|
||||
}
|
||||
|
||||
wx.hideLoading();
|
||||
wx.showToast({
|
||||
title: '保存成功',
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
});
|
||||
|
||||
// 刷新全局用户信息缓存
|
||||
try {
|
||||
await apiClient.refreshUserInfoCache();
|
||||
} catch (error) {
|
||||
console.error('刷新全局用户信息缓存失败:', error);
|
||||
}
|
||||
|
||||
// 更新全局用户信息缓存
|
||||
try {
|
||||
const app = getApp();
|
||||
if (app.globalData.userInfo) {
|
||||
// 更新全局用户信息,包括所有字段
|
||||
const globalUserInfo = app.globalData.userInfo;
|
||||
if (globalUserInfo.user) {
|
||||
// 更新所有字段到全局用户信息(使用新字段名)
|
||||
Object.assign(globalUserInfo.user, {
|
||||
nickname: user.nickname,
|
||||
avatar: user.avatar,
|
||||
bio: user.bio,
|
||||
gender: user.gender,
|
||||
birthday: user.birthday,
|
||||
ethnicity: user.ethnicity,
|
||||
occupation: user.occupation,
|
||||
school: user.school,
|
||||
hometown: user.hometown,
|
||||
zodiacSign: user.zodiacSign,
|
||||
height: user.height,
|
||||
mbtiType: user.mbtiType,
|
||||
sleepHabit: user.sleepHabit,
|
||||
socialActivity: user.socialActivity
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新全局用户信息失败:', error);
|
||||
}
|
||||
|
||||
// 通知 profile 页面更新用户信息
|
||||
try {
|
||||
const pages = getCurrentPages();
|
||||
// 查找 profile 页面(上一个页面)
|
||||
const profilePage = pages[pages.length - 2];
|
||||
if (profilePage) {
|
||||
// 重新加载用户数据(会从全局缓存读取最新数据)
|
||||
if (typeof profilePage.loadUserData === 'function') {
|
||||
profilePage.loadUserData();
|
||||
}
|
||||
|
||||
// 直接更新 profile 页面的 userInfo,包括所有字段
|
||||
const profileUserInfo = profilePage.data.userInfo || {};
|
||||
if (!profileUserInfo.user) {
|
||||
profileUserInfo.user = {};
|
||||
}
|
||||
|
||||
// 重新计算年龄(根据生日)
|
||||
let calculatedAge = null;
|
||||
if (user.birthday && typeof profilePage.calculateAge === 'function') {
|
||||
calculatedAge = profilePage.calculateAge(user.birthday);
|
||||
}
|
||||
|
||||
// 更新所有字段
|
||||
const updateData = {
|
||||
'userInfo.user.nickname': user.nickname,
|
||||
'userInfo.user.avatar': user.avatar,
|
||||
'userInfo.user.bio': user.bio,
|
||||
'userInfo.user.gender': user.gender,
|
||||
'userInfo.user.birthday': user.birthday,
|
||||
'userInfo.user.ethnicity': user.ethnicity,
|
||||
'userInfo.user.occupation': user.occupation,
|
||||
'userInfo.user.school': user.school,
|
||||
'userInfo.user.hometown': user.hometown,
|
||||
'userInfo.user.zodiacSign': user.zodiacSign,
|
||||
'userInfo.user.height': user.height,
|
||||
'userInfo.user.mbtiType': user.mbtiType,
|
||||
'userInfo.user.sleepHabit': user.sleepHabit,
|
||||
'userInfo.user.socialActivity': user.socialActivity,
|
||||
'userInfo.avatar': user.avatar,
|
||||
'userInfo.avatarUrl': user.avatar,
|
||||
'userInfo.nickname': user.nickname,
|
||||
'userInfo.bio': user.bio,
|
||||
'userInfo.gender': user.gender,
|
||||
'userInfo.birthday': user.birthday,
|
||||
'userInfo.ethnicity': user.ethnicity,
|
||||
'userInfo.occupation': user.occupation,
|
||||
'userInfo.school': user.school,
|
||||
'userInfo.hometown': user.hometown,
|
||||
'userInfo.zodiacSign': user.zodiacSign,
|
||||
'userInfo.height': user.height,
|
||||
'userInfo.mbtiType': user.mbtiType,
|
||||
'userInfo.sleepHabit': user.sleepHabit,
|
||||
'userInfo.socialActivity': user.socialActivity
|
||||
};
|
||||
|
||||
// 如果有计算出的年龄,也更新
|
||||
if (calculatedAge !== null) {
|
||||
updateData.calculatedAge = calculatedAge;
|
||||
}
|
||||
|
||||
profilePage.setData(updateData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('通知 profile 页面更新失败:', error);
|
||||
// 不影响主流程,静默失败
|
||||
}
|
||||
|
||||
// 保存成功后,延迟返回上一页
|
||||
setTimeout(() => {
|
||||
wx.navigateBack();
|
||||
}, 1500);
|
||||
|
||||
} catch (err) {
|
||||
wx.hideLoading();
|
||||
console.error('保存用户信息失败', err);
|
||||
wx.showToast({
|
||||
title: err.message || '保存失败,请重试',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"navigationBarTitleText": "个人资料",
|
||||
"navigationBarBackgroundColor": "#000000",
|
||||
"navigationBarTextStyle": "white",
|
||||
"backgroundColor": "#000000",
|
||||
"backgroundTextStyle": "light"
|
||||
}
|
||||
160
subpackages/profile/personal-details/personal-details.wxml
Normal file
160
subpackages/profile/personal-details/personal-details.wxml
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
<view class="personal">
|
||||
<scroll-view class="personal-container"
|
||||
scroll-y="true"
|
||||
enhanced="true"
|
||||
bounces="true"
|
||||
show-scrollbar="false"
|
||||
refresher-enabled="true"
|
||||
refresher-triggered="{{refreshing}}"
|
||||
bindrefresherrefresh="onRefresh">
|
||||
<!-- 头像 + customId + 昵称 -->
|
||||
<view class="card intro-card">
|
||||
<view class="row">
|
||||
<text class="left">头像</text>
|
||||
<view class="right">
|
||||
<image src="{{user.avatar}}" class="avatar-img" mode="aspectFill" bindtap="chooseAvatar"/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="row">
|
||||
<text class="left">昵称</text>
|
||||
<view class="right">
|
||||
<input
|
||||
class="input"
|
||||
placeholder="请输入昵称"
|
||||
value="{{user.nickname}}"
|
||||
maxlength="15"
|
||||
bindinput="onNicknameInput"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 个人简介 -->
|
||||
<text class="left" style="position:relative;top:-12rpx;">个人简介</text>
|
||||
<view class="card intro-card">
|
||||
<textarea
|
||||
class="textarea"
|
||||
value="{{user.bio}}"
|
||||
bindinput="onIntroInput"
|
||||
maxlength="150"
|
||||
placeholder="请填写个人简介(最多150字)"
|
||||
></textarea>
|
||||
</view>
|
||||
|
||||
<!-- 关于我 -->
|
||||
<text class="left">关于我</text>
|
||||
<view class="card intro-card">
|
||||
<view class="row" bindtap="openSheet" data-key="occupation" data-title="选择职业" data-options="{{occupationOptions}}">
|
||||
<text class="left">职业</text>
|
||||
<view class="right">
|
||||
<text class="value">{{user.occupation || '请选择'}}</text>
|
||||
<text class="chev">›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="row">
|
||||
<text class="left">毕业院校</text>
|
||||
<view class="right">
|
||||
<input
|
||||
class="input"
|
||||
placeholder="请输入毕业院校"
|
||||
value="{{user.school}}"
|
||||
maxlength="100"
|
||||
bindinput="onSchoolInput"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="row" bindtap="openSheet" data-key="gender" data-title="选择性别" data-options="{{genderOptions}}">
|
||||
<text class="left">性别</text>
|
||||
<view class="right">
|
||||
<text class="value">{{genderDisplayText}}</text>
|
||||
<text class="chev">›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="row">
|
||||
<text class="left">民族</text>
|
||||
<view class="right">
|
||||
<input
|
||||
class="input"
|
||||
placeholder="请输入民族"
|
||||
value="{{user.ethnicity}}"
|
||||
maxlength="50"
|
||||
bindinput="onEthnicityInput"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="row">
|
||||
<text class="left">生日</text>
|
||||
<view class="right">
|
||||
<picker mode="date" value="{{user.birthday}}" start="1900-01-01" end="2100-12-31" bindchange="onBirthdayChange">
|
||||
<text class="value">{{user.birthday || '请选择'}}</text>
|
||||
</picker>
|
||||
<text class="chev">›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="row">
|
||||
<text class="left">家乡</text>
|
||||
<view class="right">
|
||||
<picker mode="region" value="{{user.hometown}}" bindchange="onHometownChange">
|
||||
<text class="value" wx:if="{{!user.hometown || user.hometown.length === 0}}">请选择</text>
|
||||
<view class="value">{{user.hometown[0]}} {{user.hometown[1]}} {{user.hometown[2]}}</view>
|
||||
</picker>
|
||||
<text class="chev">›</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 更多 -->
|
||||
<text class="left">更多</text>
|
||||
<view class="card intro-card">
|
||||
<view class="row">
|
||||
<text class="left">星座</text>
|
||||
<view class="right"><text style="color:#bfbfbf;font-size: 28rpx;">{{user.zodiacSign || '选择生日后会自动试算哦😘'}}</text></view>
|
||||
</view>
|
||||
<view class="row" bindtap="openSheet" data-key="height" data-title="选择身高" data-options="{{heightOptions}}">
|
||||
<text class="left">身高</text>
|
||||
<view class="right"><text class="value">{{user.height || '请选择'}} cm</text><text class="chev">›</text></view>
|
||||
</view>
|
||||
<view class="row" bindtap="openSheet" data-key="mbtiType" data-title="选择MBTI类型" data-options="{{mbtiTypeOptions}}">
|
||||
<text class="left">MBTI类型</text>
|
||||
<view class="right"><text class="value">{{user.mbtiType || '请选择'}}</text><text class="chev">›</text></view>
|
||||
</view>
|
||||
<view class="row" bindtap="openSheet" data-key="sleepHabit" data-title="选择睡眠习惯" data-options="{{sleepHabitOptions}}">
|
||||
<text class="left">睡眠习惯</text>
|
||||
<view class="right"><text class="value">{{user.sleepHabit || '请选择'}}</text><text class="chev">›</text></view>
|
||||
</view>
|
||||
<view class="row" bindtap="openSheet" data-key="socialActivity" data-title="选择社交活跃度" data-options="{{socialActivityOptions}}">
|
||||
<text class="left">社交活跃度</text>
|
||||
<view class="right"><text class="value">{{user.socialActivity || '请选择'}}</text><text class="chev">›</text></view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 保存按钮 -->
|
||||
<view class="save-button-container">
|
||||
<view class="save-button" bind:tap="handleSave">保存</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部内联选择面板(sheet) -->
|
||||
<view wx:if="{{showSheet}}">
|
||||
<view class="mask" bindtap="closeSheet"></view>
|
||||
<view class="sheet">
|
||||
<view class="sheet-handle"></view>
|
||||
<view class="sheet-title">
|
||||
<text>{{sheetTitle}}</text>
|
||||
<!-- <button class="sheet-done" bindtap="closeSheet">完成</button> -->
|
||||
</view>
|
||||
<scroll-view class="sheet-list" scroll-y="true">
|
||||
<view wx:for="{{sheetOptions}}" wx:key="index" wx:for-item="item" data-index="{{index}}" bindtap="onSheetSelect" class="sheet-item {{item==selectedValue? 'active' : ''}}">
|
||||
<text class="sheet-item-text">{{item}}</text>
|
||||
<text wx:if="{{item==selectedValue}}" class="sheet-check">✓</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
282
subpackages/profile/personal-details/personal-details.wxss
Normal file
282
subpackages/profile/personal-details/personal-details.wxss
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
/* 全局背景与字体 */
|
||||
page, .personal {
|
||||
height: 100%;
|
||||
background: #000000;
|
||||
padding: 30rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 容器 */
|
||||
.personal-container {
|
||||
/* flex: 1; */
|
||||
background: transparent;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* 卡片基础 */
|
||||
.card {
|
||||
background: rgb(105 105 105 / 30%);
|
||||
border-radius: 18rpx;
|
||||
/* 减小整体内边距,给行内元素更多可用宽度 */
|
||||
padding: 12rpx 14rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 6rpx 18rpx rgba(0,0,0,0.6);
|
||||
color: #e8e8e8;
|
||||
}
|
||||
|
||||
/* 头像区域 */
|
||||
.avatar-area {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.avatar-area .left .label {
|
||||
font-size: 26rpx;
|
||||
color: #cfcfcf;
|
||||
}
|
||||
/* .avatar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
} */
|
||||
.avatar-img {
|
||||
width: 110rpx;
|
||||
height: 110rpx;
|
||||
border-radius: 50%;
|
||||
background: #000000;
|
||||
margin-left: 18rpx;
|
||||
margin-right: 18rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 头像外层容器(如有使用) */
|
||||
.avatar-wrap {
|
||||
width: 110rpx;
|
||||
height: 110rpx;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
margin-left: 18rpx;
|
||||
margin-right: 18rpx;
|
||||
box-shadow: 0 6rpx 18rpx rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
/* 头像占位样式(无头像时) */
|
||||
.avatar-placeholder {
|
||||
width: 110rpx;
|
||||
height: 110rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #151516 0%, #0F0F11 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #e8e8e8;
|
||||
font-size: 40rpx;
|
||||
margin-left: 18rpx;
|
||||
margin-right: 18rpx;
|
||||
box-shadow: 0 6rpx 18rpx rgba(0,0,0,0.4);
|
||||
}
|
||||
.chev {
|
||||
color: #9b9b9b;
|
||||
font-size: 30rpx;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
/* 小按钮样式 */
|
||||
.icon-btn {
|
||||
/* 小按钮,移除强制宽度,允许在小屏上收缩 */
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 28rpx;
|
||||
width: 120rpx;
|
||||
padding: 4rpx 8rpx;
|
||||
font-size: 26rpx;
|
||||
background-color: rgb(143 49 255 / 32%);
|
||||
box-sizing: border-box;
|
||||
flex: 0 0 auto;
|
||||
line-height: 1;
|
||||
}
|
||||
/* 输入框样式 */
|
||||
.input {
|
||||
font-size: 24rpx;
|
||||
/* 可见的文本颜色 */
|
||||
color: #ffffff;
|
||||
/* 右对齐输入 */
|
||||
text-align: right;
|
||||
flex: 1 1 auto; /* 主动占满剩余空间 */
|
||||
width: auto;
|
||||
min-width: 0; /* 允许在 flex 容器中收缩,避免换行 */
|
||||
border: none; /* 移除默认边框 */
|
||||
background: transparent;
|
||||
padding: 0 0 0 8rpx;
|
||||
}
|
||||
|
||||
/* 个人简介 */
|
||||
.intro-box {
|
||||
background: rgb(105 105 105 / 30%);
|
||||
border-radius: 12rpx;
|
||||
padding: 16rpx;
|
||||
min-height: 140rpx;
|
||||
}
|
||||
.intro-text {
|
||||
color: #bfbfbf;
|
||||
line-height: 1.6;
|
||||
font-size: 24rpx;
|
||||
white-space: normal;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* 编辑态 textarea */
|
||||
.textarea {
|
||||
width: 95%;
|
||||
min-height: 80rpx;
|
||||
max-height: 200rpx;
|
||||
background: #0b0d0e;
|
||||
color: #ddd;
|
||||
border-radius: 12rpx;
|
||||
padding: 16rpx;
|
||||
font-size: 24rpx;
|
||||
border: 1rpx solid rgba(255,255,255,0.02);
|
||||
}
|
||||
.intro-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
.btn {
|
||||
padding: 10rpx 20rpx;
|
||||
margin-left: 12rpx;
|
||||
border-radius: 12rpx;
|
||||
font-size: 24rpx;
|
||||
border: none;
|
||||
}
|
||||
.cancel {
|
||||
background: transparent;
|
||||
color: #9b9b9b;
|
||||
border: 1rpx solid rgba(255,255,255,0.03);
|
||||
}
|
||||
.save {
|
||||
background: linear-gradient(90deg,#00c2a8,#00a3ff);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 卡片样式 */
|
||||
.intro-card .row {
|
||||
display: flex;
|
||||
flex-wrap: nowrap; /* 禁止换行,保证左侧标签与右侧内容在同一行 */
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
/* 减小行内上下与左右内边距,释放水平空间 */
|
||||
padding: 22rpx 8rpx;
|
||||
border-bottom: 1rpx solid rgba(255,255,255,0.02);
|
||||
}
|
||||
.intro-card .row:last-child { border-bottom: none; }
|
||||
.left { color: #cfcfcf; font-size: 26rpx; /* 固定或最小宽度,避免被压缩换行 */
|
||||
/* 将左侧最小宽度适度减小,给输入留出更多空间 */
|
||||
min-width: 88rpx;
|
||||
flex: 0 0 auto;
|
||||
white-space: nowrap; /* 防止标签内换行 */
|
||||
}
|
||||
.right { display:flex; align-items:center; flex: 1 1 auto; justify-content: flex-end; gap: 6rpx; }
|
||||
.value { color: #e3e3e3; font-size: 26rpx; margin-right: 10rpx; }
|
||||
|
||||
/* 底部选择面板(sheet)与遮罩 */
|
||||
.mask {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.5);
|
||||
z-index: 50;
|
||||
}
|
||||
.sheet {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 60;
|
||||
background: #0f1112;
|
||||
border-top-left-radius: 28rpx;
|
||||
border-top-right-radius: 28rpx;
|
||||
padding: 18rpx;
|
||||
box-shadow: 0 -8rpx 30rpx rgba(0,0,0,0.6);
|
||||
}
|
||||
.sheet-handle {
|
||||
width: 80rpx;
|
||||
height: 6rpx;
|
||||
background: rgba(255,255,255,0.06);
|
||||
border-radius: 6rpx;
|
||||
margin: 6rpx auto 12rpx;
|
||||
}
|
||||
.sheet-title {
|
||||
display:flex;
|
||||
justify-content:space-between;
|
||||
align-items:center;
|
||||
padding-bottom: 12rpx;
|
||||
border-bottom: 1rpx solid rgba(255,255,255,0.03);
|
||||
}
|
||||
.sheet-title text { color: white; font-size: 28rpx; }
|
||||
.sheet-done {
|
||||
background: transparent;
|
||||
color: #9aa0a6;
|
||||
border: none;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.sheet-list {
|
||||
max-height: 420rpx;
|
||||
margin-top: 12rpx;
|
||||
padding-bottom: 12rpx;
|
||||
}
|
||||
.sheet-item {
|
||||
padding: 16rpx 12rpx;
|
||||
display:flex;
|
||||
justify-content:space-between;
|
||||
align-items:center;
|
||||
border-bottom: 1rpx solid rgba(255,255,255,0.02);
|
||||
font-size: 26rpx;
|
||||
color: #d7d7d7;
|
||||
}
|
||||
.sheet-item.active {
|
||||
background: rgba(0,160,255,0.06);
|
||||
color: #00a3ff;
|
||||
}
|
||||
.sheet-check { color: #00a3ff; font-size: 28rpx; }
|
||||
|
||||
/* 保存按钮容器 - 参考 phone-next 样式 */
|
||||
.save-button-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 40rpx;
|
||||
box-sizing: border-box;
|
||||
padding: 0 30rpx;
|
||||
}
|
||||
|
||||
.save-button {
|
||||
height: 100rpx;
|
||||
width: 240rpx;
|
||||
background: linear-gradient(135deg, #000000, #232323);
|
||||
color: white;
|
||||
text-align: center;
|
||||
line-height: 100rpx;
|
||||
border: 1rpx solid #4e4e4e;
|
||||
border-radius: 60rpx;
|
||||
font-size: 40rpx;
|
||||
font-weight: 500;
|
||||
box-shadow:
|
||||
4rpx 4rpx 30rpx rgba(91, 196, 245, 0.1),
|
||||
0 4rpx 8rpx rgba(255, 255, 255, 0.2) inset;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.save-button:active {
|
||||
box-shadow:
|
||||
0 -16rpx 32rpx rgba(18, 158, 240, 0.1) inset, /* 内层上阴影(凹陷) */
|
||||
0 4rpx 8rpx rgba(0, 0, 0, 0.1) inset; /* 内层下阴影 */
|
||||
transform: translateY(4rpx); /* 向下偏移,强化"按下"感 */
|
||||
}
|
||||
2656
subpackages/profile/profile/profile.js
Normal file
2656
subpackages/profile/profile/profile.js
Normal file
File diff suppressed because it is too large
Load diff
5
subpackages/profile/profile/profile.json
Normal file
5
subpackages/profile/profile/profile.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"navigationBarTitleText": "个人资料",
|
||||
"navigationBarTextStyle": "white",
|
||||
"disableScroll": true
|
||||
}
|
||||
537
subpackages/profile/profile/profile.wxml
Normal file
537
subpackages/profile/profile/profile.wxml
Normal file
|
|
@ -0,0 +1,537 @@
|
|||
<wxs module="utils" src="./profile.wxs"></wxs>
|
||||
<!--个人资料页面 - 美化版设计-->
|
||||
<view class="profile-container">
|
||||
|
||||
<scroll-view class="profile-content"
|
||||
scroll-y="true"
|
||||
enhanced="true"
|
||||
bounces="true"
|
||||
show-scrollbar="false"
|
||||
refresher-enabled="true"
|
||||
refresher-triggered="{{refreshing}}"
|
||||
bindrefresherrefresh="onRefresh"
|
||||
bindscrolltolower="loadMoreDynamics"
|
||||
lower-threshold="100">
|
||||
|
||||
<!-- 个人信息卡片(改版) -->
|
||||
<view class="profile-card">
|
||||
<!-- 顶部区域:头像、名字、ID、去认证 -->
|
||||
<view class="profile-top">
|
||||
<view class="avatar-section">
|
||||
<view class="avatar-container" bindtap="changeAvatar">
|
||||
<image class="avatar-image"
|
||||
src="{{selfAvatar || '/images/default-stranger.png'}}"
|
||||
mode="aspectFill" />
|
||||
<view class="avatar-decoration" wx:if="{{userInfo.isVip}}">👑</view>
|
||||
<view class="online-status online"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="profile-main-info">
|
||||
<view class="profile-name-row">
|
||||
<view class="profile-name">{{userInfo.nickname || 'FindMe用户'}}</view>
|
||||
<view class="vip-crown" wx:if="{{userInfo.isMember}}">👑</view>
|
||||
</view>
|
||||
|
||||
<view class="profile-id">
|
||||
<text class="id-label">ID: </text>
|
||||
<view class="id-value-wrapper" bindtap="copyId">
|
||||
<text class="id-value">{{userInfo.customId}}</text>
|
||||
</view>
|
||||
<!-- 新增:去认证按钮 -->
|
||||
<view class="verify-btn" wx:if="{{!userInfo.verified}}">
|
||||
<image class="verify-btn-p" src="/images/btn.png" mode="widthFix" />
|
||||
<text class="verify-text">去认证</text>
|
||||
</view>
|
||||
<view class="verified-tag" wx:if="{{userInfo.verified}}">
|
||||
<image class="verified-tag-p" src="/images/tag.png" mode="widthFix" />
|
||||
<text class="verified-text">已认证</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部信息区:位置、二维码、编辑按钮、设置、简介、个人标签 -->
|
||||
<view class="profile-bottom">
|
||||
<!-- 操作按钮区 -->
|
||||
<view class="action-buttons">
|
||||
<view class="qr-code-btn" bindtap="navigateToQRCode">
|
||||
<image src="/images/qr-code.png" class="qr-code-icon"/>
|
||||
</view>
|
||||
|
||||
<view class="edit-btn" bindtap="editProfile">
|
||||
<image src="/images/Edit3.png" class="edit-icon"/>
|
||||
<text class="edit-text">编辑</text>
|
||||
</view>
|
||||
|
||||
<view class="setting-btn" bindtap="openSettings">
|
||||
<image src="/images/Subtract.png" class="setting-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>
|
||||
|
||||
<!-- 个人简介 wx:if="{{!userInfo.verified}}"-->
|
||||
<view class="profile-signature">
|
||||
<text class="signature-text">{{userInfo.bio || '暂无个人简介'}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 个人标签区域 -->
|
||||
<view class="profile-tabs">
|
||||
<scroll-view scroll-x="true" class="tab-scroll" enable-flex="true">
|
||||
<view class="tab-item {{selectedTab === 'gender' ? 'active' : ''}}" data-tab="gender" bindtap="onTabSelect" wx:if="{{userInfo.gender !== null && userInfo.gender !== undefined && userInfo.gender !== ''}}">
|
||||
<image wx:if="{{userInfo.gender === 1 || userInfo.gender === '1' || userInfo.gender === 2 || userInfo.gender === '2'}}" class="gender-icon" src="{{userInfo.gender === 1 || userInfo.gender === '1' ? '/images/self/male.svg' : '/images/self/fmale.svg'}}" mode="aspectFit" />
|
||||
<text wx:else>?</text>
|
||||
</view>
|
||||
|
||||
<view class="tab-item {{selectedTab === 'age' ? 'active' : ''}}" data-tab="age" bindtap="onTabSelect" wx:if="{{calculatedAge !== null && calculatedAge !== undefined || userInfo.age}}">
|
||||
<text>{{calculatedAge !== null && calculatedAge !== undefined ? calculatedAge : ((userInfo.age + "岁") || '')}}</text>
|
||||
</view>
|
||||
|
||||
<view class="tab-item {{selectedTab === 'mood' ? 'active' : ''}}" data-tab="mood" bindtap="onTabSelect" wx:if="{{userInfo.mood && userInfo.mood !== ''}}">
|
||||
<text>{{userInfo.mood || ''}}</text>
|
||||
</view>
|
||||
|
||||
<view class="tab-item {{selectedTab === 'mbtiType' ? 'active' : ''}}" data-tab="mbtiType" bindtap="onTabSelect" wx:if="{{userInfo.mbtiType && userInfo.mbtiType !== ''}}">
|
||||
<text>{{userInfo.mbtiType || ''}}</text>
|
||||
</view>
|
||||
|
||||
<view class="tab-item {{selectedTab === 'identity' ? 'active' : ''}}" data-tab="identity" bindtap="onTabSelect" wx:if="{{userInfo.identity && userInfo.identity !== ''}}">
|
||||
<text>{{userInfo.identity || ''}}</text>
|
||||
</view>
|
||||
|
||||
<view class="tab-item {{selectedTab === 'zodiacSign' ? 'active' : ''}}" data-tab="zodiacSign" bindtap="onTabSelect" wx:if="{{userInfo.zodiacSign && userInfo.zodiacSign !== ''}}">
|
||||
<text>{{userInfo.zodiacSign || ''}}</text>
|
||||
</view>
|
||||
|
||||
<view class="tab-item {{selectedTab === 'school' ? 'active' : ''}}" data-tab="school" bindtap="onTabSelect" wx:if="{{userInfo.school && userInfo.school !== ''}}">
|
||||
<text>{{userInfo.school || ''}}</text>
|
||||
</view>
|
||||
|
||||
<view class="tab-item {{selectedTab === 'occupation' ? 'active' : ''}}" data-tab="occupation" bindtap="onTabSelect" wx:if="{{userInfo.occupation && userInfo.occupation !== ''}}">
|
||||
<text>{{userInfo.occupation || ''}}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- VIP 权益卡片 -->
|
||||
<!-- <view class="vip-benefit-card">
|
||||
<view class="vip-left">
|
||||
<image class="vip-logo" src="/images/self/findme_logo_self.png" mode="aspectFit"/>
|
||||
<view class="vip-text">发现你的专属</view>
|
||||
<view class="vip-text vip-sub-text">开通VIP会员尊享特权</view>
|
||||
<view class="vip-trial-btn" bindtap="navigateToVIP">
|
||||
<text>立即开通</text>
|
||||
<image src="/images/self/small.png" class="arrow-right-icon"></image>
|
||||
</view>
|
||||
</view>
|
||||
<image class="vip-icon" src="/images/self/vip.png" mode="aspectFit"/>
|
||||
</view> -->
|
||||
|
||||
<!-- My Footprints -->
|
||||
<!-- <view class="myfootprint" >
|
||||
<view class="footprint-title">我的足迹</view>
|
||||
<image class="footprint-earth" src="/images/self/earth.png" mode="aspectFit"/>
|
||||
<view class="footprint-badge">
|
||||
<text>1个NOW,2个地点</text>
|
||||
</view>
|
||||
</view> -->
|
||||
|
||||
<!-- 自定义功能模块(渐变背景,3列x2行)- 已隐藏 -->
|
||||
<view class="self-tools-module">
|
||||
<view class="self-tools-grid">
|
||||
<block wx:for="{{selfTools}}" wx:key="label">
|
||||
<view class="self-tools-item" bindtap="onSelfToolClick" data-url="{{item.url}}" data-label="{{item.label}}">
|
||||
<view class="self-tools-box">
|
||||
<image src="{{item.icon}}" mode="aspectFit"/>
|
||||
<text>{{item.label}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 动态标题-->
|
||||
<text class="dynamics-title">我的动态 {{formattedDynamicCount}}</text>
|
||||
|
||||
<!-- 发动态按钮 -->
|
||||
<view class="post-button" bindtap="handlePostImage">
|
||||
<text class="post-button-text">写点最近的动态...</text>
|
||||
<image class="post-camera-icon" src="/images/cam.svg" mode="aspectFit" />
|
||||
</view>
|
||||
|
||||
<!-- 我的动态模块 -->
|
||||
<view class="dynamics-module" wx:if="{{totalDynamicCount > 0 || isLoadingMore || dynamicList.length > 0}}">
|
||||
|
||||
<!-- 动态列表-->
|
||||
<view class="dynamics-list">
|
||||
<!-- 动态区块 -->
|
||||
<view class="dynamic-block" wx:for="{{dynamicList}}" wx:key="id" wx:for-item="dynamic" wx:for-index="feedIndex">
|
||||
<view class="dynamic-item">
|
||||
|
||||
<!-- 动态发布时间 -->
|
||||
<view class="dynamic-time">
|
||||
<text class="dynamic-date">{{dynamic.formattedTime.date}}</text>
|
||||
<text class="dynamic-year" wx:if="{{dynamic.formattedTime.year}}">{{dynamic.formattedTime.year}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 动态文字内容 -->
|
||||
<view class="dynamic-content" wx:if="{{dynamic.content}}">
|
||||
<text>{{dynamic.content}}</text>
|
||||
</view>
|
||||
<!-- 动态图片 -->
|
||||
<view class="dynamic-image-container {{dynamic.media.length > 1 ? 'multi-img' : ''}}"
|
||||
wx:if="{{dynamic.media && dynamic.media.length > 0}}">
|
||||
<block wx:for="{{dynamic.media}}" wx:key="index" wx:for-item="mediaItem" wx:for-index="mediaIndex">
|
||||
<view class="dynamic-image-wrapper">
|
||||
<image
|
||||
class="dynamic-image"
|
||||
src="{{mediaItem.thumbnailUrl || mediaItem.url || '/images/placeholder.png'}}"
|
||||
mode="aspectFill"
|
||||
bindtap="previewDynamicImage"
|
||||
data-index="{{mediaIndex}}"
|
||||
data-feed-index="{{feedIndex}}"
|
||||
/>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<!-- 动态位置 -->
|
||||
<view class="dynamic-location" wx:if="{{dynamic.location && dynamic.location.name}}">
|
||||
<image src="/images/loca.svg" class="location-icon-sm"/>
|
||||
<text class="location-text-sm">{{dynamic.location.name}}{{dynamic.location.address && dynamic.location.address !== dynamic.location.name ? ' · ' + dynamic.location.address : ''}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 点赞和评论功能 -->
|
||||
<view class="feed-actions">
|
||||
<view class="action-item" bindtap="handleLike" data-isliked="{{dynamic.isLiked}}" data-feed-uuid="{{dynamic.uuid || dynamic.id || dynamic.dynamicId}}" data-feed-index="{{feedIndex}}">
|
||||
<image class="like-icon" src="{{dynamic.isLiked ? '/images/like-active.svg' : '/images/like.svg'}}" mode="aspectFit"></image>
|
||||
<text class="action-count">{{dynamic.interactions.likeCount || 0}}</text>
|
||||
</view>
|
||||
<view class="action-item" bindtap="handleComment" data-feed-uuid="{{dynamic.uuid || dynamic.id || dynamic.dynamicId}}" data-feed-index="{{feedIndex}}">
|
||||
<text class="action-button-text">添加评论</text>
|
||||
<text class="action-count">{{dynamic.interactions.commentCount || 0}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 评论显示区域 -->
|
||||
<scroll-view
|
||||
class="feed-comments-container"
|
||||
wx:if="{{dynamic.comments && dynamic.comments.length > 0}}"
|
||||
scroll-y="{{true}}"
|
||||
bindscrolltolower="onCommentScrollToLower"
|
||||
data-feed-index="{{feedIndex}}"
|
||||
lower-threshold="100"
|
||||
>
|
||||
<!-- 一级评论 -->
|
||||
<view class="feed-comment-item" wx:for="{{utils.slice(dynamic.comments, dynamic.visibleCommentCount || 20)}}" wx:key="id" wx:for-index="commentIndex" wx:for-item="comment">
|
||||
<view class="feed-comment-content">
|
||||
<text class="feed-comment-name">{{comment.user ? comment.user.nickname : '未知用户'}}</text>
|
||||
<text class="feed-comment-separator"> - </text>
|
||||
<text class="feed-comment-text">{{comment.content}}</text>
|
||||
</view>
|
||||
<view class="feed-comment-footer">
|
||||
<text class="feed-comment-time" wx:if="{{comment.formattedTime}}">{{comment.formattedTime}}</text>
|
||||
<text class="feed-comment-reply" wx:if="{{!comment.isOwn}}" bindtap="handleReplyClick" data-feed-index="{{feedIndex}}" data-comment-id="{{comment.id}}" data-comment-index="{{commentIndex}}">回复</text>
|
||||
<text class="feed-comment-delete" wx:if="{{comment.isOwn}}" bindtap="deleteComment" data-feed-index="{{feedIndex}}" data-comment-id="{{comment.id}}" data-comment-index="{{commentIndex}}">删除</text>
|
||||
</view>
|
||||
|
||||
<!-- 二级回复列表 -->
|
||||
<view class="feed-replies-container" wx:if="{{comment.replies && comment.replies.length > 0}}">
|
||||
<view class="feed-reply-item" wx:for="{{utils.slice(comment.replies, comment.visibleReplyCount || 5)}}" wx:key="id" wx:for-index="replyIndex" wx:for-item="reply">
|
||||
<view class="feed-reply-content">
|
||||
<!-- 用户名 回复 被回复用户名 -->
|
||||
<view class="feed-reply-header">
|
||||
<text class="feed-reply-name">{{reply.user ? reply.user.nickname : '未知用户'}}</text>
|
||||
<text wx:if="{{reply.replyToUser}}" class="feed-reply-to"> 回复 </text>
|
||||
<text wx:if="{{reply.replyToUser}}" class="feed-reply-to-name">{{reply.replyToUser.nickname || '未知用户'}}</text>
|
||||
</view>
|
||||
<!-- 回复内容(可换行) -->
|
||||
<view class="feed-reply-body">
|
||||
<text class="feed-comment-text">{{reply.content}}</text>
|
||||
</view>
|
||||
<!-- 时间和回复按钮(换行显示) -->
|
||||
<view class="feed-reply-footer">
|
||||
<text class="feed-comment-time" wx:if="{{reply.formattedTime}}">{{reply.formattedTime}}</text>
|
||||
<text class="feed-comment-reply" wx:if="{{!reply.isOwn}}" bindtap="handleReplyClick" data-feed-index="{{feedIndex}}" data-comment-id="{{comment.id}}" data-comment-index="{{commentIndex}}" data-reply-id="{{reply.id}}" data-reply-to-user="{{reply.user ? reply.user.nickname : ''}}">回复</text>
|
||||
<text class="feed-comment-delete" wx:if="{{reply.isOwn}}" bindtap="deleteReply" data-feed-index="{{feedIndex}}" data-comment-id="{{comment.id}}" data-comment-index="{{commentIndex}}" data-reply-id="{{reply.id}}" data-reply-index="{{replyIndex}}">删除</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 二级回复的回复输入框 -->
|
||||
<view class="reply-input-container" wx:if="{{showReplyInput['feed_' + feedIndex + '_comment_' + comment.id + '_reply_' + reply.id]}}">
|
||||
<input
|
||||
class="reply-input"
|
||||
placeholder="说点什么..."
|
||||
value="{{replyInputValue}}"
|
||||
bindinput="onReplyInput"
|
||||
confirm-type="send"
|
||||
bindconfirm="submitReply"
|
||||
data-feed-index="{{feedIndex}}"
|
||||
data-comment-id="{{comment.id}}"
|
||||
data-comment-index="{{commentIndex}}"
|
||||
data-reply-id="{{reply.id}}"
|
||||
disabled="{{submittingReply}}"
|
||||
/>
|
||||
<view class="reply-send-btn {{submittingReply ? 'reply-send-btn-disabled' : ''}}" bindtap="submitReply" data-feed-index="{{feedIndex}}" data-comment-id="{{comment.id}}" data-comment-index="{{commentIndex}}" data-reply-id="{{reply.id}}">
|
||||
<text class="reply-send-text">{{submittingReply ? '发送中...' : '发送'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 上划查看更多回复提示 -->
|
||||
<view class="scroll-more-hint" wx:if="{{comment.replies.length > (comment.visibleReplyCount || 5)}}">
|
||||
<text class="scroll-more-text">上划查看更多回复</text>
|
||||
</view>
|
||||
<!-- 展开更多回复按钮 -->
|
||||
<view class="expand-replies-btn" wx:if="{{comment.replies.length > (comment.visibleReplyCount || 5)}}" bindtap="expandReplies" data-feed-index="{{feedIndex}}" data-comment-index="{{commentIndex}}">
|
||||
<text class="expand-replies-text">——展开更多回复</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 回复输入框 -->
|
||||
<view class="reply-input-container" wx:if="{{showReplyInput['feed_' + feedIndex + '_comment_' + comment.id]}}">
|
||||
<input
|
||||
class="reply-input"
|
||||
placeholder="说点什么..."
|
||||
value="{{replyInputValue}}"
|
||||
bindinput="onReplyInput"
|
||||
confirm-type="send"
|
||||
bindconfirm="submitReply"
|
||||
data-feed-index="{{feedIndex}}"
|
||||
data-comment-id="{{comment.id}}"
|
||||
data-comment-index="{{commentIndex}}"
|
||||
disabled="{{submittingReply}}"
|
||||
/>
|
||||
<view class="reply-send-btn {{submittingReply ? 'reply-send-btn-disabled' : ''}}" bindtap="submitReply" data-feed-index="{{feedIndex}}" data-comment-id="{{comment.id}}" data-comment-index="{{commentIndex}}">
|
||||
<text class="reply-send-text">{{submittingReply ? '发送中...' : '发送'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="expand-comments-btn" wx:if="{{dynamic.comments.length > (dynamic.visibleCommentCount || 20)}}" bindtap="expandComments" data-feed-index="{{feedIndex}}">
|
||||
<text class="expand-comments-text">——展开更多回复</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 评论输入框 -->
|
||||
<view class="reply-input-container" wx:if="{{showCommentInput['feed_' + feedIndex]}}">
|
||||
<input
|
||||
class="reply-input"
|
||||
placeholder="说点什么..."
|
||||
value="{{commentInputValue}}"
|
||||
bindinput="onCommentInput"
|
||||
confirm-type="send"
|
||||
bindconfirm="submitComment"
|
||||
data-feed-index="{{feedIndex}}"
|
||||
disabled="{{submittingComment}}"
|
||||
/>
|
||||
<view class="reply-send-btn {{submittingComment ? 'reply-send-btn-disabled' : ''}}" bindtap="submitComment" data-feed-index="{{feedIndex}}">
|
||||
<text class="reply-send-text">{{submittingComment ? '发送中...' : '发送'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载中提示 -->
|
||||
<view class="dynamics-loading" wx:if="{{isLoadingMore}}">
|
||||
<view class="loading-spinner"></view>
|
||||
<text class="loading-text">加载更多...</text>
|
||||
</view>
|
||||
|
||||
<!-- 已加载全部提示 -->
|
||||
<view class="dynamics-no-more" wx:if="{{!isLoadingMore && !hasMoreData}}">
|
||||
<text class="no-more-text">已显示全部动态</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 调试信息卡片 -->
|
||||
<view class="debug-card" wx:if="{{debugMode}}">
|
||||
<view class="debug-header">
|
||||
<view class="debug-icon">🔧</view>
|
||||
<text class="debug-title">调试信息</text>
|
||||
<view class="debug-toggle" bindtap="toggleDebug">
|
||||
<text class="toggle-text">{{showDebugDetails ? '隐藏' : '展开'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="debug-content" wx:if="{{showDebugDetails}}">
|
||||
<view class="debug-section">
|
||||
<text class="debug-label">用户信息:</text>
|
||||
<text class="debug-value">{{debugInfo.userInfo}}</text>
|
||||
</view>
|
||||
|
||||
<view class="debug-section">
|
||||
<text class="debug-label">Token状态:</text>
|
||||
<text class="debug-value {{debugInfo.tokenValid ? 'success' : 'error'}}">
|
||||
{{debugInfo.tokenValid ? '✅ 有效' : '❌ 无效'}}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="debug-section">
|
||||
<text class="debug-label">网络状态:</text>
|
||||
<text class="debug-value">{{debugInfo.networkType || '未知'}}</text>
|
||||
</view>
|
||||
|
||||
<view class="debug-section">
|
||||
<text class="debug-label">版本信息:</text>
|
||||
<text class="debug-value">{{debugInfo.version || 'v1.0.0'}}</text>
|
||||
</view>
|
||||
|
||||
<view class="debug-actions">
|
||||
<view class="debug-btn" bindtap="testAPI">
|
||||
<text class="btn-text">测试API</text>
|
||||
</view>
|
||||
<view class="debug-btn" bindtap="showCacheStats">
|
||||
<text class="btn-text">缓存统计</text>
|
||||
</view>
|
||||
<view class="debug-btn" bindtap="clearCache">
|
||||
<text class="btn-text">清除缓存</text>
|
||||
</view>
|
||||
<view class="debug-btn" bindtap="exportLogs">
|
||||
<text class="btn-text">导出日志</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部空间 -->
|
||||
<view class="bottom-space" style="height: {{bottomSpaceHeight || 120}}rpx;"></view>
|
||||
</scroll-view>
|
||||
|
||||
|
||||
<!-- 个人状态编辑弹窗 -->
|
||||
<view class="status-modal {{showStatusModal ? 'show' : ''}}" wx:if="{{showStatusModal}}">
|
||||
<view class="modal-mask" bindtap="hideStatusModal"></view>
|
||||
<view class="modal-content">
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">设置状态</text>
|
||||
<view class="modal-close" bindtap="hideStatusModal">✕</view>
|
||||
</view>
|
||||
|
||||
<view class="modal-body">
|
||||
<view class="status-options">
|
||||
<view class="status-option"
|
||||
wx:for="{{statusOptions}}"
|
||||
wx:key="id"
|
||||
bindtap="selectStatus"
|
||||
data-status="{{item}}"
|
||||
class="{{selectedStatus.id === item.id ? 'selected' : ''}}">
|
||||
<view class="option-icon">{{item.icon}}</view>
|
||||
<text class="option-text">{{item.text}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="status-input">
|
||||
<input class="custom-status"
|
||||
placeholder="自定义状态..."
|
||||
value="{{customStatus}}"
|
||||
bindinput="onStatusInput" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="modal-footer">
|
||||
<view class="modal-btn cancel" bindtap="hideStatusModal">取消</view>
|
||||
<view class="modal-btn confirm" bindtap="saveStatus">保存</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 成就徽章展示 -->
|
||||
<view class="achievement-banner" wx:if="{{showAchievement}}">
|
||||
<view class="banner-content">
|
||||
<view class="achievement-icon">🏆</view>
|
||||
<view class="achievement-text">
|
||||
<text class="achievement-title">恭喜获得新徽章!</text>
|
||||
<text class="achievement-desc">{{newAchievement.name}}</text>
|
||||
</view>
|
||||
<view class="banner-close" bindtap="hideAchievement">✕</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- VIP会员选择弹窗 -->
|
||||
<view class="vip-modal-overlay" wx:if="{{showVipModal}}" bindtap="closeVipModal" catchtouchmove="preventTouchMove">
|
||||
<view class="vip-modal-container" catchtap="stopPropagation">
|
||||
<!-- 弹窗标题 -->
|
||||
<view class="vip-modal-header">
|
||||
<text class="vip-modal-title">开通VIP会员</text>
|
||||
<text class="vip-modal-subtitle">尊享专属特权,解锁更多精彩</text>
|
||||
<view class="vip-modal-close" bindtap="closeVipModal">✕</view>
|
||||
</view>
|
||||
|
||||
<!-- 套餐列表 -->
|
||||
<scroll-view class="vip-packages-scroll" scroll-y="true" enhanced="true">
|
||||
<view class="vip-packages-list">
|
||||
<view
|
||||
class="vip-package-item {{selectedVipPackage && selectedVipPackage.id === item.id ? 'selected' : ''}}"
|
||||
wx:for="{{vipPackages}}"
|
||||
wx:key="id"
|
||||
data-id="{{item.id}}"
|
||||
bindtap="selectVipPackage">
|
||||
|
||||
<!-- 推荐标签 -->
|
||||
<view class="vip-hot-tag" wx:if="{{item.hot}}">🔥 推荐</view>
|
||||
|
||||
<!-- 套餐主信息 -->
|
||||
<view class="vip-package-main">
|
||||
<view class="vip-package-name">{{item.name}}</view>
|
||||
<view class="vip-package-duration">{{item.duration}}</view>
|
||||
|
||||
<view class="vip-package-price-section">
|
||||
<view class="vip-package-price">
|
||||
<text class="vip-price-symbol">¥</text>
|
||||
<text class="vip-price-value">{{item.price}}</text>
|
||||
</view>
|
||||
<view class="vip-package-original-price">¥{{item.originalPrice}}</view>
|
||||
</view>
|
||||
|
||||
<view class="vip-package-discount" wx:if="{{item.discount}}">{{item.discount}}</view>
|
||||
</view>
|
||||
|
||||
<!-- 权益列表 -->
|
||||
<view class="vip-benefits-list">
|
||||
<view class="vip-benefit-item" wx:for="{{item.benefits}}" wx:key="index" wx:for-item="benefit">
|
||||
<text class="vip-benefit-icon">✓</text>
|
||||
<text class="vip-benefit-text">{{benefit}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 查看详情按钮 -->
|
||||
<view class="vip-detail-btn" data-id="{{item.id}}" catchtap="viewVipBenefits">
|
||||
查看详情 →
|
||||
</view>
|
||||
|
||||
<!-- 选中指示器 -->
|
||||
<view class="vip-selected-indicator" wx:if="{{selectedVipPackage && selectedVipPackage.id === item.id}}">
|
||||
<view class="vip-selected-checkmark">✓</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部购买按钮 -->
|
||||
<view class="vip-modal-footer">
|
||||
<view class="vip-purchase-info" wx:if="{{selectedVipPackage}}">
|
||||
<text class="vip-purchase-label">已选择:</text>
|
||||
<text class="vip-purchase-package">{{selectedVipPackage.name}}</text>
|
||||
<text class="vip-purchase-price">¥{{selectedVipPackage.price}}</text>
|
||||
</view>
|
||||
<view class="vip-purchase-info" wx:else>
|
||||
<text class="vip-purchase-hint">请选择会员套餐</text>
|
||||
</view>
|
||||
|
||||
<button
|
||||
class="vip-purchase-btn {{selectedVipPackage ? 'active' : 'disabled'}}"
|
||||
bindtap="purchaseVip"
|
||||
disabled="{{!selectedVipPackage}}">
|
||||
立即开通
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
19
subpackages/profile/profile/profile.wxs
Normal file
19
subpackages/profile/profile/profile.wxs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
// 截取数组的前n个元素
|
||||
function slice(array, count) {
|
||||
if (!array) {
|
||||
return [];
|
||||
}
|
||||
var len = array.length;
|
||||
if (len === 0) {
|
||||
return [];
|
||||
}
|
||||
var num = parseInt(count) || 5;
|
||||
if (num <= 0) {
|
||||
num = 5;
|
||||
}
|
||||
return array.slice(0, num > len ? len : num);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
slice: slice
|
||||
};
|
||||
2346
subpackages/profile/profile/profile.wxss
Normal file
2346
subpackages/profile/profile/profile.wxss
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue