Files
soul-yongping/miniprogram/pages/profile-edit/profile-edit.js
Alex-larget 219ae3b843 删除过时的批处理脚本和部署Python文件
- 删除了macOS虚拟机的安装和迁移批处理脚本,因为它们已不再需要。
- 删除了macOS虚拟机的导出脚本,以简化项目流程。
- 删除了soul-admin项目的部署Python脚本,以简化代码库。
- 更新了小程序,以反映环境配置的变化并提升用户体验。
2026-03-16 16:10:30 +08:00

425 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Soul创业派对 - 资料编辑完整版comprehensive_profile_editor_v1_1
* 温馨提示、头像、基本信息、核心联系方式、个人故事、互助需求、项目介绍
*
* 接口约定(/api/miniprogram/user/profile
* - GET ?userId= 返回 data: { avatar, nickname, mbti, region, industry, businessScale, position, skills, phone, wechatId, ... },空值统一为 ""
* - POST body普通用户提交基础字段VIP 提交全部字段(含 skills、个人故事、互助需求、项目介绍
*
* 表单展示:普通用户仅展示 温馨提示、头像、昵称、MBTI、地区、行业、业务体量、职位、核心联系方式VIP 展示全部
*/
const app = getApp()
const MBTI_OPTIONS = ['INTJ', 'INFP', 'INTP', 'ENTP', 'ENFP', 'ENTJ', 'ENFJ', 'INFJ', 'ISTJ', 'ISFJ', 'ESTJ', 'ESFJ', 'ISTP', 'ISFP', 'ESTP', 'ESFP']
Page({
data: {
statusBarHeight: 44,
isVip: false,
avatar: '',
nickname: '',
shareCardPath: '', // 分享名片封面图(预生成)
mbti: '',
mbtiIndex: 0,
region: '',
industry: '',
businessScale: '',
position: '',
skills: '',
phone: '',
wechatId: '',
storyBestMonth: '',
storyAchievement: '',
storyTurning: '',
helpOffer: '',
helpNeed: '',
projectIntro: '',
mbtiOptions: MBTI_OPTIONS,
showMbtiPicker: false,
saving: false,
loading: true,
showAvatarModal: false,
},
onLoad(options) {
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 })
wx.showShareMenu({ withShareTimeline: true })
// 从朋友圈/分享打开且带 id跳转到名片详情member-detail
if (options?.id) {
const ref = options.ref ? `&ref=${options.ref}` : ''
wx.redirectTo({ url: `/pages/member-detail/member-detail?id=${options.id}${ref}` })
return
}
this.loadProfile()
},
async loadProfile() {
const userInfo = app.globalData.userInfo
if (!app.globalData.isLoggedIn || !userInfo?.id) {
this.setData({ loading: false })
wx.showToast({ title: '请先登录', icon: 'none' })
setTimeout(() => getApp().goBackOrToHome(), 1500)
return
}
try {
const userId = userInfo.id
const [profileRes, vipRes] = await Promise.all([
app.request({ url: `/api/miniprogram/user/profile?userId=${userId}`, silent: true }),
app.request({ url: `/api/miniprogram/vip/status?userId=${userId}`, silent: true }),
])
this.setData({ isVip: vipRes?.data?.isVip || false })
const res = profileRes
if (res?.success && res.data) {
const d = res.data
const v = (k) => (d[k] != null && d[k] !== '') ? String(d[k]) : ''
const mbtiIndex = MBTI_OPTIONS.indexOf(v('mbti')) >= 0 ? MBTI_OPTIONS.indexOf(v('mbti')) : 0
this.setData({
avatar: v('avatar'),
nickname: v('nickname'),
mbti: v('mbti'),
mbtiIndex,
region: v('region'),
industry: v('industry'),
businessScale: v('businessScale'),
position: v('position'),
skills: v('skills'),
phone: v('phone'),
wechatId: v('wechatId') || wx.getStorageSync('user_wechat') || '',
storyBestMonth: v('storyBestMonth'),
storyAchievement: v('storyAchievement'),
storyTurning: v('storyTurning'),
helpOffer: v('helpOffer'),
helpNeed: v('helpNeed'),
projectIntro: v('projectIntro'),
loading: false,
})
setTimeout(() => this.generateShareCard(), 200)
} else {
this.setData({ loading: false })
}
} catch (e) {
this.setData({ loading: false })
}
},
goBack() { getApp().goBackOrToHome() },
// 生成分享名片封面图(头像+昵称5:4 比例)
async generateShareCard() {
const { avatar, nickname } = this.data
const userId = app.globalData.userInfo?.id
if (!userId) return
try {
const ctx = wx.createCanvasContext('shareCardCanvas', this)
const w = 500
const h = 400
// 背景渐变Soul 深色风格)
const grd = ctx.createLinearGradient(0, 0, w, h)
grd.addColorStop(0, '#0F172A')
grd.addColorStop(0.5, '#050B14')
grd.addColorStop(1, '#0F172A')
ctx.setFillStyle(grd)
ctx.fillRect(0, 0, w, h)
// 顶部装饰条
ctx.setFillStyle('#5EEAD4')
ctx.fillRect(0, 0, w, 4)
// 头像:居中偏上,圆形 120px
const avatarSize = 120
const avatarX = (w - avatarSize) / 2
const avatarY = 80
const avatarRadius = avatarSize / 2
const drawAvatar = () => new Promise((resolve) => {
if (avatar && avatar.startsWith('http')) {
wx.downloadFile({
url: avatar,
success: (res) => {
if (res.statusCode === 200) {
ctx.save()
ctx.beginPath()
ctx.arc(avatarX + avatarRadius, avatarY + avatarRadius, avatarRadius, 0, Math.PI * 2)
ctx.clip()
ctx.drawImage(res.tempFilePath, avatarX, avatarY, avatarSize, avatarSize)
ctx.restore()
} else {
this.drawAvatarPlaceholder(ctx, avatarX, avatarY, avatarSize, nickname)
}
resolve()
},
fail: () => {
this.drawAvatarPlaceholder(ctx, avatarX, avatarY, avatarSize, nickname)
resolve()
},
})
} else {
this.drawAvatarPlaceholder(ctx, avatarX, avatarY, avatarSize, nickname)
resolve()
}
})
await drawAvatar()
// 头像外圈描边
ctx.setStrokeStyle('#5EEAD4')
ctx.setLineWidth(3)
ctx.beginPath()
ctx.arc(avatarX + avatarRadius, avatarY + avatarRadius, avatarRadius, 0, Math.PI * 2)
ctx.stroke()
// 昵称
const displayName = (nickname || '').trim() || '创业者'
ctx.setFillStyle('#ffffff')
ctx.setFontSize(24)
ctx.setTextAlign('center')
ctx.fillText(displayName, w / 2, avatarY + avatarSize + 50)
// 副标题
ctx.setFillStyle('rgba(94,234,212,0.9)')
ctx.setFontSize(14)
ctx.fillText('Soul创业派对 · 名片', w / 2, avatarY + avatarSize + 78)
ctx.draw(true, () => {
wx.canvasToTempFilePath({
canvasId: 'shareCardCanvas',
destWidth: 500,
destHeight: 400,
success: (res) => {
this.setData({ shareCardPath: res.tempFilePath })
},
}, this)
})
} catch (e) {
console.warn('[ShareCard] 生成失败:', e)
}
},
drawAvatarPlaceholder(ctx, x, y, size, nickname) {
ctx.setFillStyle('rgba(94,234,212,0.25)')
ctx.beginPath()
ctx.arc(x + size / 2, y + size / 2, size / 2, 0, Math.PI * 2)
ctx.fill()
ctx.setFillStyle('#5EEAD4')
ctx.setFontSize(size * 0.45)
ctx.setTextAlign('center')
ctx.fillText((nickname || '?')[0], x + size / 2, y + size / 2 + size * 0.15)
},
onShareAppMessage() {
const ref = app.getMyReferralCode()
const userId = app.globalData.userInfo?.id
const nickname = (this.data.nickname || '').trim() || '我'
const path = userId
? (ref ? `/pages/member-detail/member-detail?id=${userId}&ref=${ref}` : `/pages/member-detail/member-detail?id=${userId}`)
: (ref ? `/pages/profile-edit/profile-edit?ref=${ref}` : '/pages/profile-edit/profile-edit')
const result = {
title: `${nickname}为您分享名片`,
path,
}
if (this.data.shareCardPath) result.imageUrl = this.data.shareCardPath
return result
},
onShareTimeline() {
const ref = app.getMyReferralCode()
const userId = app.globalData.userInfo?.id
const nickname = (this.data.nickname || '').trim() || '我'
const query = userId
? (ref ? `id=${userId}&ref=${ref}` : `id=${userId}`)
: (ref ? `ref=${ref}` : '')
const result = {
title: `${nickname}为您分享名片`,
query: query || '',
}
if (this.data.shareCardPath) result.imageUrl = this.data.shareCardPath
return result
},
onNicknameInput(e) { this.setData({ nickname: e.detail.value }) },
onNicknameChange(e) { this.setData({ nickname: e.detail.value }) },
onRegionInput(e) { this.setData({ region: e.detail.value }) },
onIndustryInput(e) { this.setData({ industry: e.detail.value }) },
onBusinessScaleInput(e) { this.setData({ businessScale: e.detail.value }) },
onPositionInput(e) { this.setData({ position: e.detail.value }) },
onSkillsInput(e) { this.setData({ skills: e.detail.value }) },
onPhoneInput(e) { this.setData({ phone: e.detail.value }) },
onWechatInput(e) { this.setData({ wechatId: e.detail.value }) },
onStoryBestMonthInput(e) { this.setData({ storyBestMonth: e.detail.value }) },
onStoryAchievementInput(e) { this.setData({ storyAchievement: e.detail.value }) },
onStoryTurningInput(e) { this.setData({ storyTurning: e.detail.value }) },
onHelpOfferInput(e) { this.setData({ helpOffer: e.detail.value }) },
onHelpNeedInput(e) { this.setData({ helpNeed: e.detail.value }) },
onProjectIntroInput(e) { this.setData({ projectIntro: e.detail.value }) },
onMbtiPickerChange(e) {
const i = parseInt(e.detail.value, 10)
this.setData({ mbtiIndex: i, mbti: MBTI_OPTIONS[i] })
},
// 点击头像:选择微信头像或从相册选择
onAvatarTap() {
wx.showActionSheet({
itemList: ['使用微信头像', '从相册选择'],
success: (res) => {
if (res.tapIndex === 0) {
this.setData({ showAvatarModal: true })
} else if (res.tapIndex === 1) {
this.chooseAvatarFromAlbum()
}
},
})
},
closeAvatarModal() {
this.setData({ showAvatarModal: false })
},
// 从相册/相机选择头像
chooseAvatarFromAlbum() {
wx.chooseMedia({
count: 1,
mediaType: ['image'],
sourceType: ['album', 'camera'],
success: async (res) => {
const tempPath = res.tempFiles[0].tempFilePath
wx.showLoading({ title: '上传中...', mask: true })
try {
const uploadRes = await new Promise((resolve, reject) => {
wx.uploadFile({
url: app.globalData.baseUrl + '/api/miniprogram/upload',
filePath: tempPath,
name: 'file',
formData: { folder: 'avatars' },
success: (r) => {
try {
const data = JSON.parse(r.data)
if (data.success) resolve(data)
else reject(new Error(data.error || '上传失败'))
} catch { reject(new Error('解析失败')) }
},
fail: reject,
})
})
const avatarUrl = app.globalData.baseUrl + uploadRes.data.url
this.setData({ avatar: avatarUrl })
await app.request({
url: '/api/miniprogram/user/profile',
method: 'POST',
data: { userId: app.globalData.userInfo?.id, avatar: avatarUrl },
})
if (app.globalData.userInfo) {
app.globalData.userInfo.avatar = avatarUrl
wx.setStorageSync('userInfo', app.globalData.userInfo)
}
wx.hideLoading()
wx.showToast({ title: '头像已更新', icon: 'success' })
setTimeout(() => this.generateShareCard(), 200)
} catch (e) {
wx.hideLoading()
wx.showToast({ title: e.message || '上传失败', icon: 'none' })
}
},
})
},
// 微信原生 chooseAvatar 回调:使用当前微信头像
async onChooseAvatar(e) {
const tempAvatarUrl = e.detail?.avatarUrl
this.setData({ showAvatarModal: false })
if (!tempAvatarUrl) return
wx.showLoading({ title: '上传中...', mask: true })
try {
const uploadRes = await new Promise((resolve, reject) => {
wx.uploadFile({
url: app.globalData.baseUrl + '/api/miniprogram/upload',
filePath: tempAvatarUrl,
name: 'file',
formData: { folder: 'avatars' },
success: (r) => {
try {
const data = JSON.parse(r.data)
if (data.success) resolve(data)
else reject(new Error(data.error || '上传失败'))
} catch {
reject(new Error('解析失败'))
}
},
fail: reject,
})
})
const avatarUrl = app.globalData.baseUrl + uploadRes.data.url
this.setData({ avatar: avatarUrl })
await app.request({
url: '/api/miniprogram/user/profile',
method: 'POST',
data: { userId: app.globalData.userInfo?.id, avatar: avatarUrl },
})
if (app.globalData.userInfo) {
app.globalData.userInfo.avatar = avatarUrl
wx.setStorageSync('userInfo', app.globalData.userInfo)
}
wx.hideLoading()
wx.showToast({ title: '头像已更新', icon: 'success' })
setTimeout(() => this.generateShareCard(), 200)
} catch (err) {
wx.hideLoading()
wx.showToast({ title: err.message || '上传失败,请重试', icon: 'none' })
}
},
async saveProfile() {
const userId = app.globalData.userInfo?.id
if (!userId) {
wx.showToast({ title: '请先登录', icon: 'none' })
return
}
this.setData({ saving: true })
try {
const s = (v) => (v || '').toString().trim()
const isVip = this.data.isVip
const payload = {
userId,
avatar: s(this.data.avatar),
nickname: s(this.data.nickname),
mbti: s(this.data.mbti),
region: s(this.data.region),
industry: s(this.data.industry),
businessScale: s(this.data.businessScale),
position: s(this.data.position),
phone: s(this.data.phone),
wechatId: s(this.data.wechatId),
}
const showHelp = isVip || this.data.helpOffer || this.data.helpNeed
if (isVip) {
payload.skills = s(this.data.skills)
payload.storyBestMonth = s(this.data.storyBestMonth)
payload.storyAchievement = s(this.data.storyAchievement)
payload.storyTurning = s(this.data.storyTurning)
payload.projectIntro = s(this.data.projectIntro)
}
if (showHelp) {
payload.helpOffer = s(this.data.helpOffer)
payload.helpNeed = s(this.data.helpNeed)
}
if (payload.wechatId) wx.setStorageSync('user_wechat', payload.wechatId)
if (payload.phone) wx.setStorageSync('user_phone', payload.phone)
const hasUpdate = Object.keys(payload).some(k => k !== 'userId' && payload[k] !== '')
if (!hasUpdate) {
wx.showToast({ title: '无变更', icon: 'none' })
this.setData({ saving: false })
return
}
await app.request({
url: '/api/miniprogram/user/profile',
method: 'POST',
data: payload,
})
wx.showToast({ title: '保存成功', icon: 'success' })
if (app.globalData.userInfo) {
if (payload.nickname) app.globalData.userInfo.nickname = payload.nickname
if (payload.avatar) app.globalData.userInfo.avatar = payload.avatar
wx.setStorageSync('userInfo', app.globalData.userInfo)
}
setTimeout(() => getApp().goBackOrToHome(), 800)
} catch (e) {
wx.showToast({ title: e.message || '保存失败', icon: 'none' })
}
this.setData({ saving: false })
},
})