diff --git a/pages/settings/settings.js b/pages/settings/settings.js index b5f2218..eabf840 100644 --- a/pages/settings/settings.js +++ b/pages/settings/settings.js @@ -136,19 +136,34 @@ Page({ // --- Profile handlers --- - onChooseAvatar(e) { + async onChooseAvatar(e) { const avatarUrl = e.detail.avatarUrl if (!avatarUrl) return - this.setData({ avatarUrl }) - const saved = storage.saveProfile({ - nickname: this.data.nickName, - avatarUrl - }) - this.setData({ - nickName: saved.nickname || this.data.nickName, - avatarUrl: saved.avatarUrl - }) - wx.showToast({ title: '头像已更新', icon: 'success', duration: 1200 }) + try { + // chooseAvatar returns a wxfile:// temporary URL that expires in a few + // days. Read the bytes and store as a data URI so the avatar survives + // long-term. Falls back to the raw URL on size/read error. + const permanentUrl = await util.fileToDataURI(avatarUrl) + this.setData({ avatarUrl: permanentUrl }) + const saved = storage.saveProfile({ + nickname: this.data.nickName, + avatarUrl: permanentUrl + }) + this.setData({ + nickName: saved.nickname || this.data.nickName, + avatarUrl: saved.avatarUrl + }) + wx.showToast({ title: '头像已更新', icon: 'success', duration: 1200 }) + } catch (err) { + console.warn('avatar convert failed, saving temp URL:', err) + // Fallback: save the raw wxfile URL. Will eventually expire. + this.setData({ avatarUrl }) + storage.saveProfile({ + nickname: this.data.nickName, + avatarUrl + }) + wx.showToast({ title: '头像已更新', icon: 'success', duration: 1200 }) + } }, onNicknameBlur(e) { diff --git a/utils/util.js b/utils/util.js index 048ae57..7b8a27a 100644 --- a/utils/util.js +++ b/utils/util.js @@ -54,11 +54,52 @@ const formatDate = (date) => { /** Strip the time portion of a formatDate string → "YYYY-MM-DD". */ const _dateOnly = (s) => (typeof s === 'string' && s.length >= 10) ? s.substring(0, 10) : '' +/** + * Convert a local file path (typically `wxfile://...` from chooseAvatar) to a + * base64 data URI. Used to make temporary file URLs survive the WeChat + * expiration window (a few days) by embedding the bytes inline. + * + * Rejects if the file exceeds `maxBytes` (default 512KB) so a stray huge + * image doesn't blow the 10MB total wx.storage budget. Caller should + * fall back to the raw URL on rejection. + */ +const fileToDataURI = (filePath, maxBytes = 512 * 1024) => { + return new Promise((resolve, reject) => { + if (!filePath || typeof wx.getFileSystemManager !== 'function') { + reject(new Error('FileSystemManager unavailable')) + return + } + const fs = wx.getFileSystemManager() + fs.getFileInfo({ + filePath, + success: (info) => { + if (info.size > maxBytes) { + reject(new Error(`File too large: ${info.size} > ${maxBytes}`)) + return + } + fs.readFile({ + filePath, + encoding: 'base64', + success: (res) => { + // chooseAvatar returns PNG; allow JPG/GIF/WEBP fallbacks. + const m = (filePath.match(/\.(\w+)(?:\?|$)/) || [, 'png'])[1].toLowerCase() + const mime = { png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp' }[m] || 'image/png' + resolve(`data:${mime};base64,${res.data}`) + }, + fail: reject + }) + }, + fail: reject + }) + }) +} + module.exports = { formatTime, formatDuration, getDaysInMonth, getMonthCalendar, formatDate, - dateOnly: _dateOnly + dateOnly: _dateOnly, + fileToDataURI }