2026-04-18 17:16:49 +08:00
|
|
|
|
const app = getApp()
|
|
|
|
|
|
|
2026-05-12 20:27:47 +08:00
|
|
|
|
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') || m.includes('合法域名')) return '域名未在公众平台配置,或未开启 HTTPS'
|
|
|
|
|
|
if (m.includes('ssl') || m.includes('certificate')) return 'HTTPS 证书异常,请检查服务器证书'
|
|
|
|
|
|
if (m.includes('timeout') || m.includes('timed out')) return '连接超时,请稍后重试'
|
|
|
|
|
|
return '网络异常,请检查网络后重试'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-07 10:19:07 +08:00
|
|
|
|
const DEFAULT_AVATAR = 'data:image/svg+xml;utf8,' + encodeURIComponent(`
|
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="320" height="320" viewBox="0 0 320 320">
|
|
|
|
|
|
<defs>
|
|
|
|
|
|
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
|
|
|
|
|
<stop offset="0%" stop-color="#c19cff"/>
|
|
|
|
|
|
<stop offset="100%" stop-color="#00e3fd"/>
|
|
|
|
|
|
</linearGradient>
|
|
|
|
|
|
</defs>
|
|
|
|
|
|
<rect width="320" height="320" rx="160" fill="#1d2025"/>
|
|
|
|
|
|
<circle cx="160" cy="160" r="154" fill="url(#bg)" opacity="0.18"/>
|
|
|
|
|
|
<circle cx="160" cy="126" r="54" fill="#f6f6fc" opacity="0.92"/>
|
|
|
|
|
|
<path d="M72 268c18-44 52-70 88-70s70 26 88 70" fill="#f6f6fc" opacity="0.92"/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
`)
|
|
|
|
|
|
|
2026-04-18 17:16:49 +08:00
|
|
|
|
Page({
|
|
|
|
|
|
data: {
|
|
|
|
|
|
name: '',
|
2026-04-23 11:34:54 +08:00
|
|
|
|
nickname: '',
|
2026-04-18 17:16:49 +08:00
|
|
|
|
days: 0,
|
|
|
|
|
|
badges: 0,
|
2026-04-19 10:04:00 +08:00
|
|
|
|
openid: '',
|
2026-04-23 11:34:54 +08:00
|
|
|
|
avatarUrl: '',
|
2026-05-07 10:19:07 +08:00
|
|
|
|
defaultAvatarUrl: DEFAULT_AVATAR,
|
2026-04-19 11:56:49 +08:00
|
|
|
|
loading: false,
|
|
|
|
|
|
error: '',
|
2026-04-23 11:34:54 +08:00
|
|
|
|
showNicknameModal: false,
|
|
|
|
|
|
nicknameDraft: '',
|
2026-04-18 17:16:49 +08:00
|
|
|
|
},
|
2026-04-19 10:04:00 +08:00
|
|
|
|
onShow() {
|
|
|
|
|
|
this.loadProfile()
|
|
|
|
|
|
},
|
2026-04-19 11:56:49 +08:00
|
|
|
|
async loadProfile() {
|
2026-05-12 20:27:47 +08:00
|
|
|
|
const openid = app.globalData.openid || wx.getStorageSync('openid') || ''
|
|
|
|
|
|
if (!openid) {
|
|
|
|
|
|
this.setData({
|
|
|
|
|
|
loading: false,
|
|
|
|
|
|
openid: '',
|
|
|
|
|
|
name: '未登录',
|
|
|
|
|
|
nickname: '未登录',
|
|
|
|
|
|
avatarUrl: '',
|
|
|
|
|
|
nicknameDraft: '',
|
|
|
|
|
|
badges: 0,
|
|
|
|
|
|
days: 0,
|
|
|
|
|
|
error: '',
|
|
|
|
|
|
})
|
2026-04-19 11:56:49 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-05-12 20:27:47 +08:00
|
|
|
|
this.setData({ loading: true, error: '' })
|
|
|
|
|
|
app.globalData.openid = openid
|
|
|
|
|
|
const baseUrl = app.getApiBaseUrl ? app.getApiBaseUrl() : app.globalData.baseUrl
|
|
|
|
|
|
let profileErrMsg = ''
|
2026-04-23 11:34:54 +08:00
|
|
|
|
wx.request({
|
2026-05-12 20:27:47 +08:00
|
|
|
|
url: `${baseUrl}/api/v1/auth/profile?openid=${encodeURIComponent(openid)}`,
|
2026-04-23 11:34:54 +08:00
|
|
|
|
method: 'GET',
|
|
|
|
|
|
success: (res) => {
|
2026-05-12 20:27:47 +08:00
|
|
|
|
const body = res.data || {}
|
|
|
|
|
|
const ok = res.statusCode >= 200 && res.statusCode < 300 && body.code === 0 && body.data
|
|
|
|
|
|
if (!ok) {
|
|
|
|
|
|
profileErrMsg = '个人信息加载失败,请稍后重试'
|
|
|
|
|
|
const storedAvatarUrl = wx.getStorageSync(`avatarUrl:${openid}`) || ''
|
|
|
|
|
|
const normalizedAvatar = normalizeAvatarUrl(storedAvatarUrl, baseUrl)
|
|
|
|
|
|
this.setData({
|
|
|
|
|
|
openid,
|
|
|
|
|
|
name: '考研战士',
|
|
|
|
|
|
nickname: '考研战士',
|
|
|
|
|
|
avatarUrl: normalizedAvatar,
|
|
|
|
|
|
nicknameDraft: '考研战士',
|
|
|
|
|
|
error: profileErrMsg,
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const profile = body.data
|
|
|
|
|
|
const avatarUrl = wx.getStorageSync(`avatarUrl:${openid}`) || profile.avatar_url || ''
|
2026-05-07 10:19:07 +08:00
|
|
|
|
const nickname = profile.nickname || '考研战士'
|
2026-05-12 20:27:47 +08:00
|
|
|
|
const normalizedAvatar = normalizeAvatarUrl(avatarUrl, baseUrl)
|
|
|
|
|
|
profileErrMsg = ''
|
2026-04-23 11:34:54 +08:00
|
|
|
|
this.setData({
|
2026-05-12 20:27:47 +08:00
|
|
|
|
openid,
|
2026-04-23 11:34:54 +08:00
|
|
|
|
name: nickname,
|
|
|
|
|
|
nickname,
|
|
|
|
|
|
avatarUrl: normalizedAvatar,
|
|
|
|
|
|
nicknameDraft: nickname,
|
2026-05-07 10:19:07 +08:00
|
|
|
|
days: Number(profile.days || 0),
|
|
|
|
|
|
error: '',
|
2026-04-23 11:34:54 +08:00
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
fail: () => {
|
2026-05-12 20:27:47 +08:00
|
|
|
|
profileErrMsg = '个人信息加载失败,请稍后重试'
|
|
|
|
|
|
const storedAvatarUrl = wx.getStorageSync(`avatarUrl:${openid}`) || ''
|
|
|
|
|
|
const normalizedAvatar = normalizeAvatarUrl(storedAvatarUrl, baseUrl)
|
2026-05-07 10:19:07 +08:00
|
|
|
|
this.setData({
|
2026-05-12 20:27:47 +08:00
|
|
|
|
openid,
|
2026-05-07 10:19:07 +08:00
|
|
|
|
name: '考研战士',
|
|
|
|
|
|
nickname: '考研战士',
|
|
|
|
|
|
avatarUrl: normalizedAvatar,
|
|
|
|
|
|
nicknameDraft: '考研战士',
|
2026-05-12 20:27:47 +08:00
|
|
|
|
error: profileErrMsg,
|
2026-05-07 10:19:07 +08:00
|
|
|
|
})
|
2026-04-23 11:34:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
complete: () => {
|
2026-05-12 20:27:47 +08:00
|
|
|
|
this.loadAchievements(profileErrMsg)
|
2026-04-23 11:34:54 +08:00
|
|
|
|
this.setData({ loading: false })
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
openNicknameModal() {
|
2026-05-07 10:19:07 +08:00
|
|
|
|
this.setData({ showNicknameModal: true, nicknameDraft: this.data.nickname || this.data.name || '考研战士' })
|
2026-04-23 11:34:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
onAvatarError() {
|
2026-05-12 20:27:47 +08:00
|
|
|
|
this.setData({ avatarUrl: this.data.defaultAvatarUrl })
|
2026-04-23 11:34:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
closeNicknameModal() {
|
2026-05-07 10:19:07 +08:00
|
|
|
|
this.setData({ showNicknameModal: false, nicknameDraft: this.data.nickname || this.data.name || '考研战士' })
|
2026-04-23 11:34:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
onNicknameInput(e) {
|
|
|
|
|
|
this.setData({ nicknameDraft: e.detail.value })
|
|
|
|
|
|
},
|
|
|
|
|
|
saveNickname() {
|
|
|
|
|
|
if (!app.globalData.openid) return
|
|
|
|
|
|
const nickname = (this.data.nicknameDraft || '').trim()
|
|
|
|
|
|
if (!nickname) {
|
2026-05-07 10:19:07 +08:00
|
|
|
|
wx.showToast({ title: '请输入昵称', icon: 'none' })
|
2026-04-23 11:34:54 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
wx.request({
|
2026-05-12 20:27:47 +08:00
|
|
|
|
url: `${app.getApiBaseUrl ? app.getApiBaseUrl() : app.globalData.baseUrl}/api/v1/auth/profile`,
|
2026-04-23 11:34:54 +08:00
|
|
|
|
method: 'PUT',
|
|
|
|
|
|
header: { 'Content-Type': 'application/json' },
|
|
|
|
|
|
data: { openid: app.globalData.openid, nickname },
|
|
|
|
|
|
success: (res) => {
|
|
|
|
|
|
const profile = res.data?.data || {}
|
|
|
|
|
|
const nextNickname = profile.nickname || nickname
|
|
|
|
|
|
this.setData({ nickname: nextNickname, name: nextNickname, nicknameDraft: nextNickname, showNicknameModal: false })
|
2026-05-07 10:19:07 +08:00
|
|
|
|
wx.showToast({ title: '保存成功', icon: 'success' })
|
2026-04-23 11:34:54 +08:00
|
|
|
|
},
|
2026-05-07 10:19:07 +08:00
|
|
|
|
fail: () => wx.showToast({ title: '保存失败', icon: 'none' }),
|
2026-04-23 11:34:54 +08:00
|
|
|
|
})
|
|
|
|
|
|
},
|
2026-05-12 20:27:47 +08:00
|
|
|
|
onChooseAvatar(e) {
|
|
|
|
|
|
const tempPath = e.detail?.avatarUrl || ''
|
2026-04-23 11:34:54 +08:00
|
|
|
|
if (!app.globalData.openid) {
|
2026-05-07 10:19:07 +08:00
|
|
|
|
wx.showToast({ title: '请先登录', icon: 'none' })
|
2026-04-23 11:34:54 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-05-12 20:27:47 +08:00
|
|
|
|
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?openid=${encodeURIComponent(app.globalData.openid)}`,
|
|
|
|
|
|
filePath: tempPath,
|
|
|
|
|
|
name: 'file',
|
|
|
|
|
|
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)
|
|
|
|
|
|
wx.setStorageSync(`avatarUrl:${app.globalData.openid}`, full)
|
|
|
|
|
|
this.setData({ avatarUrl: full })
|
|
|
|
|
|
wx.showToast({ title: '头像已更新', icon: 'success' })
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
wx.showToast({ title: '头像上传结果异常', icon: 'none' })
|
|
|
|
|
|
}
|
2026-04-23 11:34:54 +08:00
|
|
|
|
},
|
2026-05-12 20:27:47 +08:00
|
|
|
|
fail: (err) => wx.showToast({ title: requestFailHint(err.errMsg), icon: 'none' }),
|
2026-04-23 11:34:54 +08:00
|
|
|
|
})
|
2026-04-19 11:56:49 +08:00
|
|
|
|
},
|
2026-05-12 20:27:47 +08:00
|
|
|
|
loadAchievements(profileErrMsg) {
|
2026-04-19 11:56:49 +08:00
|
|
|
|
if (!app.globalData.openid) {
|
2026-05-07 10:19:07 +08:00
|
|
|
|
this.setData({ error: '登录状态已失效,请重新进入页面' })
|
2026-04-19 11:56:49 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-05-12 20:27:47 +08:00
|
|
|
|
const baseUrl = app.getApiBaseUrl ? app.getApiBaseUrl() : app.globalData.baseUrl
|
|
|
|
|
|
const achFail = '成就数据加载失败,请稍后重试'
|
2026-04-19 10:04:00 +08:00
|
|
|
|
wx.request({
|
2026-05-12 20:27:47 +08:00
|
|
|
|
url: `${baseUrl}/api/v1/achievements`,
|
2026-04-19 11:56:49 +08:00
|
|
|
|
header: { 'X-OpenID': app.globalData.openid },
|
2026-04-19 10:04:00 +08:00
|
|
|
|
success: (res) => {
|
2026-05-12 20:27:47 +08:00
|
|
|
|
const body = res.data || {}
|
|
|
|
|
|
const ok = res.statusCode >= 200 && res.statusCode < 300 && body.code === 0 && Array.isArray(body.data)
|
|
|
|
|
|
if (!ok) {
|
|
|
|
|
|
this.setData({ error: profileErrMsg ? `${profileErrMsg};${achFail}` : achFail })
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const list = body.data
|
|
|
|
|
|
const badges = list.filter((i) => i.is_unlocked).length
|
|
|
|
|
|
this.setData({ badges, error: profileErrMsg || '' })
|
|
|
|
|
|
},
|
|
|
|
|
|
fail: (err) => {
|
|
|
|
|
|
const netHint = requestFailHint(err.errMsg)
|
|
|
|
|
|
const line = profileErrMsg ? `${profileErrMsg};${achFail}` : achFail
|
|
|
|
|
|
this.setData({ error: `${line}(${netHint})` })
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
manualLogin() {
|
|
|
|
|
|
wx.login({
|
|
|
|
|
|
success: (res) => {
|
|
|
|
|
|
const code = res.code || ''
|
|
|
|
|
|
console.log('[login] wx.login success', res)
|
|
|
|
|
|
console.log('[login] code', code)
|
|
|
|
|
|
if (!code) {
|
|
|
|
|
|
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
wx.request({
|
|
|
|
|
|
url: `${app.getApiBaseUrl ? app.getApiBaseUrl() : app.globalData.baseUrl}/api/v1/auth/wx-login`,
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
header: { 'Content-Type': 'application/json' },
|
|
|
|
|
|
data: { code },
|
|
|
|
|
|
success: (loginRes) => {
|
|
|
|
|
|
console.log('[login] wx-login success', loginRes)
|
|
|
|
|
|
const payload = loginRes.data?.data || {}
|
|
|
|
|
|
if (loginRes.statusCode >= 200 && loginRes.statusCode < 300 && loginRes.data?.code === 0 && payload.openid) {
|
|
|
|
|
|
app.globalData.openid = payload.openid
|
|
|
|
|
|
wx.setStorageSync('openid', payload.openid)
|
|
|
|
|
|
wx.showToast({ title: '登录成功', icon: 'success' })
|
|
|
|
|
|
this.loadProfile()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
console.log('[login] wx-login invalid response', loginRes.data)
|
|
|
|
|
|
wx.showToast({ title: '登录接口异常,请稍后重试', icon: 'none' })
|
|
|
|
|
|
},
|
|
|
|
|
|
fail: (err) => {
|
|
|
|
|
|
console.log('[login] wx-login fail', err)
|
|
|
|
|
|
wx.showToast({ title: requestFailHint(err.errMsg), icon: 'none' })
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
fail: (err) => {
|
|
|
|
|
|
console.log('[login] wx.login fail', err)
|
|
|
|
|
|
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
|
2026-04-19 10:04:00 +08:00
|
|
|
|
},
|
2026-04-18 17:16:49 +08:00
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
goTo(e) {
|
|
|
|
|
|
const { url } = e.currentTarget.dataset
|
2026-04-19 11:56:49 +08:00
|
|
|
|
if (!app.globalData.openid) {
|
2026-05-07 10:19:07 +08:00
|
|
|
|
wx.showToast({ title: '请先登录', icon: 'none' })
|
2026-04-19 11:56:49 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-04-18 17:16:49 +08:00
|
|
|
|
wx.navigateTo({ url })
|
|
|
|
|
|
},
|
|
|
|
|
|
goHome() {
|
2026-04-19 16:39:02 +08:00
|
|
|
|
wx.reLaunch({ url: '/pages/index/index' })
|
2026-04-18 17:16:49 +08:00
|
|
|
|
},
|
2026-04-19 16:39:02 +08:00
|
|
|
|
goToReview() {
|
|
|
|
|
|
wx.navigateTo({ url: '/pages/review-config/review-config' })
|
|
|
|
|
|
},
|
|
|
|
|
|
goToMe() {},
|
2026-04-23 11:34:54 +08:00
|
|
|
|
noop() {},
|
2026-04-18 17:16:49 +08:00
|
|
|
|
})
|