import { request } from '../../utils/request' import * as auth from '../../utils/auth' import { logger } from '../../utils/logger' // 日志模块名 const LOG = 'login' import { STORAGE_KEYS, QuotaType } from '../../utils/config' import { LoginResult, UserInfo, QuotaItem } from '../../types/models' import { formatDate } from '../../utils/format' interface GiftItem { icon: string name: string value: string } Page({ data: { loading: false, redirect: '', // 登录后要回跳的页面(如从好友分享链接进入) userInfo: {} as UserInfo, giftVisible: false, giftItems: [] as GiftItem[], // 生日引导:v2 玻璃弹窗 birthVisible: false, birthButtons: [ { text: '稍后再说', value: '' }, { text: '去完善', value: 'ok', type: 'main' }, ], // 协议同意:默认不勾选,由用户自主阅读后自行选择(微信审核合规要求) agreed: false, consentVisible: false, consentButtons: [ { text: '不同意', value: '' }, { text: '同意并继续', value: 'ok', type: 'main' }, ], }, onLoad(query: Record) { logger.debug(LOG, 'onLoad, query=', JSON.stringify(query), 'isLoggedIn=', auth.isLoggedIn()) if (query.redirect) { // goLogin 侧对 redirect 做了 encodeURIComponent,这里解码还原(decode 失败时用原值兜底) let redirect = query.redirect try { redirect = decodeURIComponent(redirect) } catch (e) { logger.warn(LOG, 'redirect 解码失败,使用原值, err=', e) } this.setData({ redirect }) } if (auth.isLoggedIn()) { this.goAfterLogin() } }, // 登录完成后的落地页:带 redirect 时回跳来源页(分享详情等),否则进衣橱 goAfterLogin(fallback = false) { const redirect = this.data.redirect logger.debug(LOG, 'goAfterLogin, redirect=', redirect, 'fallback=', fallback) if (redirect && !fallback) { wx.redirectTo({ url: redirect, fail: (e) => { logger.warn(LOG, 'redirect 回跳失败,回退衣橱, err=', e && e.errMsg) this.goAfterLogin(true) }, }) return } wx.switchTab({ url: '/pages/wardrobe/wardrobe', fail: (e) => logger.warn(LOG, 'switchTab 衣橱失败, err=', e && e.errMsg), }) }, // 勾选 / 取消勾选协议:完全由用户操作驱动,不存在默认勾选 onToggleAgree() { const agreed = !this.data.agreed logger.debug(LOG, '协议勾选状态切换, agreed=', agreed) this.setData({ agreed }) }, // 未勾选协议点击登录:弹窗二次确认,由用户主动选择是否同意 onLogin() { if (this.data.loading) { logger.debug(LOG, '登录中,忽略重复点击') return } if (!this.data.agreed) { logger.info(LOG, '未勾选协议,弹出确认弹窗') this.setData({ consentVisible: true }) return } this.doLogin() }, onConsentCancel() { logger.debug(LOG, '用户未同意协议,停留在登录页') this.setData({ consentVisible: false }) }, onConsentConfirm(e: WechatMiniprogram.CustomEvent) { this.setData({ consentVisible: false }) // 弹窗主按钮才代表同意;其余分支(不同意)只关闭弹窗,用户仍可浏览 if (e.detail.value !== 'ok') { logger.info(LOG, '用户选择不同意协议') return } logger.info(LOG, '用户同意协议,继续登录') this.setData({ agreed: true }) this.doLogin() }, // 暂不登录:回到衣橱首页继续浏览(游客态),不产生任何授权请求 onBrowse() { logger.info(LOG, '用户选择先逛逛,返回衣橱继续浏览') wx.switchTab({ url: '/pages/wardrobe/wardrobe', fail: (e) => logger.warn(LOG, 'switchTab 衣橱失败, err=', e && e.errMsg), }) }, doLogin() { logger.debug(LOG, '点击登录, redirect=', this.data.redirect) this.setData({ loading: true }) // 超时兜底:wx.login / wx.request 异常挂起时复位按钮,避免永久 loading 点不动 this.clearLoginTimer() ;(this as any).loginTimer = setTimeout(() => { ;(this as any).loginTimer = null if (!this.data.loading) return logger.warn(LOG, '登录超时(30s),复位按钮') this.setData({ loading: false }) wx.showToast({ title: '登录超时,请重试', icon: 'none' }) }, 30000) wx.login({ success: (res) => { if (!res.code) { logger.error(LOG, 'wx.login 返回空 code') this.finishLogin() wx.showToast({ title: '登录失败,请重试', icon: 'none' }) return } logger.debug(LOG, 'wx.login 成功, codeLen=', res.code.length) request({ url: '/user/wxlogin', method: 'POST', data: { code: res.code }, auth: false, }) .then((data) => { logger.info( LOG, 'wxlogin 成功, isNew=', data.user_info && data.user_info.is_new_user, 'expireTime=', data.expire_time, ) auth.setTokens({ accessToken: data.access_token, refreshToken: data.refresh_token, expireTime: data.expire_time, }) wx.setStorageSync(STORAGE_KEYS.USER_INFO, data.user_info) // 新登录用户重置未读红点,进入 tab 页后由 tabBar 拉取真实值 wx.setStorageSync(STORAGE_KEYS.UNREAD, 0) this.setData({ userInfo: data.user_info }) if (data.user_info.is_new_user) { this.showWelcomeGift(data.user_info) } else if (!data.user_info.birth_day) { this.showBirthGuide() } else { this.goAfterLogin() } }) .catch((e) => { logger.error(LOG, 'wxlogin 失败, code=', e && e.code, 'msg=', e && e.message) }) .then(() => { // 无论成功还是失败都复位:分支走向礼包/生日引导时若不复位,登录按钮会永久点不动 this.finishLogin() }) }, fail: (e) => { logger.error(LOG, 'wx.login 失败, errMsg=', e && e.errMsg) this.finishLogin() wx.showToast({ title: '登录失败,请重试', icon: 'none' }) }, }) }, // 复位登录态:清超时定时器 + 复位按钮 finishLogin() { this.clearLoginTimer() this.setData({ loading: false }) }, clearLoginTimer() { const timer = (this as any).loginTimer if (timer) { clearTimeout(timer) ;(this as any).loginTimer = null } }, onUnload() { this.clearLoginTimer() }, showWelcomeGift(userInfo: UserInfo) { // 额度只用于礼包展示:失败静默降级,绝不能触发登出/清 token request<{ quotas: QuotaItem[] }>({ url: '/quota/info', showError: false, silentAuth: true }) .then((res) => this.setData({ giftItems: this.buildGift(userInfo, res.quotas), giftVisible: true })) .catch((e) => { // 额度拉取失败不阻塞礼包展示,降级为空额度;绝不能触发登出/清 token logger.warn(LOG, '礼包额度拉取失败,降级展示, code=', e && e.code) this.setData({ giftItems: this.buildGift(userInfo, []), giftVisible: true }) }) }, buildGift(userInfo: UserInfo, quotas: QuotaItem[]): GiftItem[] { const total = (t: number) => { const q = quotas.find((it) => it.quota_type === t) return q ? q.total : 0 } const items: GiftItem[] = [] if (userInfo.vip_type === 4 && userInfo.vip_expire) { items.push({ icon: 'icon-crown', name: '体验会员', value: `有效期至 ${formatDate(userInfo.vip_expire)}` }) } items.push({ icon: 'icon-camera', name: '上传额度', value: `${total(QuotaType.UPLOAD)} 次 · 永久` }) items.push({ icon: 'icon-sparkles', name: 'AI 推荐额度', value: `${total(QuotaType.AI)} 次/天` }) items.push({ icon: 'icon-shirt', name: '搭配分享额度', value: `${total(QuotaType.OUTFIT_SHARE)} 次/天` }) items.push({ icon: 'icon-hanger', name: '衣柜分享额度', value: `${total(QuotaType.CLOSET_SHARE)} 次/天` }) return items }, onGiftConfirm() { this.setData({ giftVisible: false }) if (!this.data.userInfo.birth_day) { this.showBirthGuide() } else { this.goAfterLogin() } }, showBirthGuide() { this.setData({ birthVisible: true }) }, onBirthCancel() { this.setData({ birthVisible: false }) this.goAfterLogin() }, onBirthConfirm(e: WechatMiniprogram.CustomEvent) { this.setData({ birthVisible: false }) if (e.detail.value !== 'ok') { this.goAfterLogin() return } // switchTab 不支持 query 传参,用 storage 标记让「我的」页 onShow 自动弹出编辑弹窗 wx.setStorageSync(STORAGE_KEYS.OPEN_PROFILE_EDIT, 1) wx.switchTab({ url: '/pages/profile/profile', fail: (err) => { logger.warn(LOG, 'switchTab 个人资料失败, err=', err && err.errMsg) // 跳转失败时清除标记,避免下次进入「我的」页意外弹窗 wx.removeStorageSync(STORAGE_KEYS.OPEN_PROFILE_EDIT) }, }) }, // 登录页协议链接:按 data-type 打开《用户协议》或《隐私政策》 openAgreement(e: WechatMiniprogram.TouchEvent) { const type = (e.currentTarget.dataset as { type?: 'user' | 'privacy' }).type === 'privacy' ? 'privacy' : 'user' logger.debug(LOG, '打开协议, type=', type) wx.navigateTo({ url: `/pages/agreement/agreement?type=${type}`, fail: (err) => logger.warn(LOG, 'navigateTo 协议页失败, err=', err && err.errMsg), }) }, })