/** * 卡若创业派对 - 资料编辑完整版(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 { toAvatarPath } = require('../../utils/util.js') 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, showPrivacyModal: false, nicknameInputFocus: false, }, onLoad(options) { this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44, fromVip: options?.from === 'vip', }) 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() }, onNicknameAreaTouch() { if (typeof wx.requirePrivacyAuthorize !== 'function') return wx.requirePrivacyAuthorize({ success: () => { this.setData({ nicknameInputFocus: true }) }, fail: () => {}, }) }, onNicknameBlur() { this.setData({ nicknameInputFocus: false }) }, preventMove() {}, handleAgreePrivacy() { if (app._privacyResolve) { app._privacyResolve({ buttonId: 'agree-btn', event: 'agree' }) app._privacyResolve = null } this.setData({ showPrivacyModal: false, nicknameInputFocus: true }) }, handleDisagreePrivacy() { if (app._privacyResolve) { app._privacyResolve({ event: 'disagree' }) app._privacyResolve = null } this.setData({ showPrivacyModal: false }) }, // 生成分享名片封面图(参考:头像左+昵称右,分隔线,四栏信息 5:4) async generateShareCard() { const { avatar, nickname, region, mbti, industry, position } = this.data const userId = app.globalData.userInfo?.id if (!userId) return try { const ctx = wx.createCanvasContext('shareCardCanvas', this) const w = 500 const h = 400 const pad = 32 // 背景(深灰卡片感) const grd = ctx.createLinearGradient(0, 0, w, h) grd.addColorStop(0, '#1E293B') grd.addColorStop(1, '#0F172A') ctx.setFillStyle(grd) ctx.fillRect(0, 0, w, h) // 顶部区域:左头像 + 右昵称 const avatarSize = 100 const avatarX = pad + 10 const avatarY = 50 const avatarRadius = avatarSize / 2 const rightStart = avatarX + avatarSize + 28 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('rgba(94,234,212,0.5)') ctx.setLineWidth(2) ctx.beginPath() ctx.arc(avatarX + avatarRadius, avatarY + avatarRadius, avatarRadius, 0, Math.PI * 2) ctx.stroke() // 右侧:昵称 + 个人名片 const displayName = (nickname || '').trim() || '创业者' ctx.setFillStyle('#ffffff') ctx.setFontSize(26) ctx.setTextAlign('left') ctx.fillText(displayName, rightStart, avatarY + 36) ctx.setFillStyle('#94A3B8') ctx.setFontSize(13) ctx.fillText('个人名片', rightStart, avatarY + 62) // 分隔线 const divY = 168 ctx.setStrokeStyle('rgba(255,255,255,0.08)') ctx.setLineWidth(1) ctx.beginPath() ctx.moveTo(pad, divY) ctx.lineTo(w - pad, divY) ctx.stroke() // 底部四栏:地区 | MBTI,行业 | 职位 const labelGray = '#64748B' const valueWhite = '#F1F5F9' const rowH = 52 const colW = (w - pad * 2) / 2 const truncate = (text, maxW) => { if (!text) return '' ctx.setFontSize(15) let m = ctx.measureText(text) if (m.width <= maxW) return text for (let i = text.length - 1; i > 0; i--) { const t = text.slice(0, i) + '…' if (ctx.measureText(t).width <= maxW) return t } return text[0] + '…' } const maxValW = colW - 16 const items = [ { label: '地区', value: truncate((region || '').trim() || '未填写', maxValW), x: pad }, { label: 'MBTI', value: (mbti || '').trim() || '未填写', x: pad + colW }, { label: '行业', value: truncate((industry || '').trim() || '未填写', maxValW), x: pad }, { label: '职位', value: truncate((position || '').trim() || '未填写', maxValW), x: pad + colW }, ] items.forEach((item, i) => { const row = Math.floor(i / 2) const baseY = divY + 36 + row * rowH ctx.setFillStyle(labelGray) ctx.setFontSize(12) ctx.fillText(item.label, item.x, baseY - 8) ctx.setFillStyle(valueWhite) ctx.setFontSize(15) ctx.fillText(item.value, item.x, baseY + 14) }) 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.2)') 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.42) ctx.setTextAlign('center') ctx.fillText((nickname || '?')[0], x + size / 2, y + size / 2 + size * 0.14) }, 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] }) }, // 微信原生 chooseAvatar 回调(点击头像直接弹出原生选择器:用微信头像/从相册选择/拍照) async onChooseAvatar(e) { const tempAvatarUrl = e.detail?.avatarUrl 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, }) }) let avatarUrl = uploadRes.data?.url || uploadRes.url if (avatarUrl && !avatarUrl.startsWith('http')) { avatarUrl = app.globalData.baseUrl + avatarUrl } this.setData({ avatar: avatarUrl }) const avatarToSave = toAvatarPath(avatarUrl) await app.request({ url: '/api/miniprogram/user/profile', method: 'POST', data: { userId: app.globalData.userInfo?.id, avatar: avatarToSave }, }) 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 } const s = (v) => (v || '').toString().trim() const isVip = this.data.isVip // 手机号必填,格式校验(支持带空格/连字符输入) const phoneRaw = s(this.data.phone) if (!phoneRaw) { wx.showToast({ title: '请输入手机号', icon: 'none' }) return } const phoneNum = phoneRaw.replace(/\D/g, '') if (!/^1[3-9]\d{9}$/.test(phoneNum)) { wx.showToast({ title: '请输入正确的11位手机号', icon: 'none' }) return } const phoneToSave = phoneNum this.setData({ saving: true }) try { const payload = { userId, avatar: toAvatarPath(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: phoneToSave, 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 } const res = 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 (res?.data?.avatar) app.globalData.userInfo.avatar = res.data.avatar wx.setStorageSync('userInfo', app.globalData.userInfo) } setTimeout(() => getApp().goBackOrToHome(), 800) } catch (e) { wx.showToast({ title: e.message || '保存失败', icon: 'none' }) } this.setData({ saving: false }) }, })