const app = getApp() function normalizeAvatarUrl(avatarUrl, baseUrl) { if (!avatarUrl) return '' return avatarUrl.startsWith('http') ? avatarUrl : `${baseUrl}${avatarUrl}` } function requestFailHint(errMsg) { const m = String(errMsg || '').toLowerCase() if (m.includes('domain')) return '域名未配置或未开启 HTTPS' if (m.includes('ssl') || m.includes('certificate')) return 'HTTPS 证书异常,请检查服务端配置' if (m.includes('timeout') || m.includes('timed out')) return '连接超时,请稍后重试' return '网络异常,请稍后重试' } const DEFAULT_AVATAR = 'data:image/svg+xml;utf8,' + encodeURIComponent(` `) Page({ data: { name: '', nickname: '', username: '', days: 0, badges: 0, avatarUrl: '', defaultAvatarUrl: DEFAULT_AVATAR, loading: false, error: '', showNicknameModal: false, nicknameDraft: '', authMode: 'login', authForm: { username: '', password: '', nickname: '', }, }, onShow() { this.loadProfile() }, isLoggedIn() { return !!app.globalData.authToken }, onAuthModeChange(e) { this.setData({ authMode: e.currentTarget.dataset.mode || 'login', error: '' }) }, onAuthInput(e) { const { field } = e.currentTarget.dataset const value = e.detail.value || '' this.setData({ [`authForm.${field}`]: value }) }, submitAuth() { const baseUrl = app.getApiBaseUrl ? app.getApiBaseUrl() : app.globalData.baseUrl const mode = this.data.authMode const username = (this.data.authForm.username || '').trim() const password = (this.data.authForm.password || '').trim() const nickname = (this.data.authForm.nickname || '').trim() if (!username || !password) { wx.showToast({ title: '请输入账号和密码', icon: 'none' }) return } if (mode === 'register' && !nickname) { wx.showToast({ title: '请输入昵称', icon: 'none' }) return } this.setData({ loading: true, error: '' }) wx.request({ url: `${baseUrl}/api/v1/auth/${mode === 'register' ? 'register' : 'login'}`, method: 'POST', header: { 'Content-Type': 'application/json' }, data: mode === 'register' ? { username, password, nickname } : { username, password }, success: (res) => { const payload = res.data?.data || null if (res.statusCode < 200 || res.statusCode >= 300 || !payload?.auth_token) { const message = res.data?.detail || res.data?.message || `${mode === 'register' ? '注册' : '登录'}失败` this.setData({ error: message }) wx.showToast({ title: message, icon: 'none' }) return } app.setLoginSession(payload) this.setData({ authForm: { username: '', password: '', nickname: '' }, username: payload.username || '', }) wx.showToast({ title: mode === 'register' ? '注册成功' : '登录成功', icon: 'success' }) this.loadProfile() }, fail: (err) => { const message = requestFailHint(err.errMsg) this.setData({ error: message }) wx.showToast({ title: message, icon: 'none' }) }, complete: () => this.setData({ loading: false }), }) }, loadProfile() { const token = app.ensureLogin() Promise.resolve(token).then((authToken) => { if (!authToken) { this.setData({ loading: false, name: '未登录', nickname: '未登录', username: '', avatarUrl: '', nicknameDraft: '', badges: 0, days: 0, error: '', }) return } this.setData({ loading: true, error: '' }) const baseUrl = app.getApiBaseUrl ? app.getApiBaseUrl() : app.globalData.baseUrl wx.request({ url: `${baseUrl}/api/v1/auth/profile`, method: 'GET', header: app.getAuthHeader(), success: (res) => { const body = res.data || {} const profile = body.data const ok = res.statusCode >= 200 && res.statusCode < 300 && body.code === 0 && profile if (!ok) { const message = body.detail || body.message || '个人信息加载失败' this.setData({ error: message }) return } const identity = profile.username || app.globalData.currentUser?.username || '' const avatarUrl = wx.getStorageSync(`avatarUrl:${identity}`) || profile.avatar_url || '' const normalizedAvatar = normalizeAvatarUrl(avatarUrl, baseUrl) const nickname = profile.nickname || identity || '考研战士' app.globalData.currentUser = { ...(app.globalData.currentUser || {}), id: profile.id, username: profile.username || '', nickname, loginType: profile.login_type || 'account', } wx.setStorageSync('currentUser', app.globalData.currentUser) this.setData({ name: nickname, nickname, username: profile.username || '', avatarUrl: normalizedAvatar, nicknameDraft: nickname, days: Number(profile.days || 0), error: '', }) this.loadAchievements() }, fail: (err) => { this.setData({ error: requestFailHint(err.errMsg) }) }, complete: () => this.setData({ loading: false }), }) }) }, openNicknameModal() { this.setData({ showNicknameModal: true, nicknameDraft: this.data.nickname || this.data.name || '考研战士' }) }, onAvatarError() { this.setData({ avatarUrl: this.data.defaultAvatarUrl }) }, closeNicknameModal() { this.setData({ showNicknameModal: false, nicknameDraft: this.data.nickname || this.data.name || '考研战士' }) }, onNicknameInput(e) { this.setData({ nicknameDraft: e.detail.value }) }, saveNickname() { if (!this.isLoggedIn()) return const nickname = (this.data.nicknameDraft || '').trim() if (!nickname) { wx.showToast({ title: '请输入昵称', icon: 'none' }) return } wx.request({ url: `${app.getApiBaseUrl ? app.getApiBaseUrl() : app.globalData.baseUrl}/api/v1/auth/profile`, method: 'PUT', header: app.getAuthHeader({ 'Content-Type': 'application/json' }), data: { nickname }, success: (res) => { const profile = res.data?.data || {} const nextNickname = profile.nickname || nickname const currentUser = { ...(app.globalData.currentUser || {}), nickname: nextNickname } app.globalData.currentUser = currentUser wx.setStorageSync('currentUser', currentUser) this.setData({ nickname: nextNickname, name: nextNickname, nicknameDraft: nextNickname, showNicknameModal: false }) wx.showToast({ title: '保存成功', icon: 'success' }) }, fail: () => wx.showToast({ title: '保存失败', icon: 'none' }), }) }, onChooseAvatar(e) { const tempPath = e.detail?.avatarUrl || '' if (!this.isLoggedIn()) { wx.showToast({ title: '请先登录', icon: 'none' }) return } if (!tempPath) { wx.showToast({ title: '未选择头像', icon: 'none' }) return } const baseUrl = app.getApiBaseUrl ? app.getApiBaseUrl() : app.globalData.baseUrl wx.uploadFile({ url: `${baseUrl}/api/v1/auth/avatar`, filePath: tempPath, name: 'file', header: app.getAuthHeader(), success: (uploadRes) => { try { const payload = JSON.parse(uploadRes.data || '{}') const ok = uploadRes.statusCode >= 200 && uploadRes.statusCode < 300 && payload.code === 0 && payload.data if (!ok) { wx.showToast({ title: payload.message || '头像上传失败', icon: 'none' }) return } const rel = payload.data.avatar_url || '' const full = normalizeAvatarUrl(rel, baseUrl) const key = this.data.username || app.globalData.currentUser?.username || '' if (key) wx.setStorageSync(`avatarUrl:${key}`, full) this.setData({ avatarUrl: full }) wx.showToast({ title: '头像已更新', icon: 'success' }) } catch { wx.showToast({ title: '头像上传结果异常', icon: 'none' }) } }, fail: (err) => wx.showToast({ title: requestFailHint(err.errMsg), icon: 'none' }), }) }, loadAchievements() { if (!this.isLoggedIn()) return const baseUrl = app.getApiBaseUrl ? app.getApiBaseUrl() : app.globalData.baseUrl wx.request({ url: `${baseUrl}/api/v1/achievements`, header: app.getAuthHeader(), success: (res) => { const body = res.data || {} const ok = res.statusCode >= 200 && res.statusCode < 300 && body.code === 0 && Array.isArray(body.data) if (!ok) return const badges = body.data.filter((i) => i.is_unlocked).length this.setData({ badges }) }, }) }, logout() { if (!this.isLoggedIn()) return wx.request({ url: `${app.getApiBaseUrl ? app.getApiBaseUrl() : app.globalData.baseUrl}/api/v1/auth/logout`, method: 'POST', header: app.getAuthHeader(), complete: () => { app.clearLoginSession() this.setData({ name: '未登录', nickname: '未登录', username: '', avatarUrl: '', nicknameDraft: '', badges: 0, days: 0, error: '', }) wx.showToast({ title: '已退出登录', icon: 'success' }) }, }) }, goTo(e) { const { url } = e.currentTarget.dataset if (!this.isLoggedIn()) { wx.showToast({ title: '请先登录', icon: 'none' }) return } wx.navigateTo({ url }) }, goHome() { wx.reLaunch({ url: '/pages/index/index' }) }, goToReview() { wx.navigateTo({ url: '/pages/review-config/review-config' }) }, goToMe() {}, noop() {}, })