381 lines
13 KiB
TypeScript
381 lines
13 KiB
TypeScript
import { request, uploadFile } from '../../utils/request'
|
|
import * as auth from '../../utils/auth'
|
|
import { UserInfo, QuotaItem } from '../../types/models'
|
|
import { QuotaType, QUOTA_NAMES, VIP_TYPE_NAMES, STORAGE_KEYS } from '../../utils/config'
|
|
import { formatDate } from '../../utils/format'
|
|
import { setTabBarSelected, setTabBarUnread, setTabBarHidden } from '../../utils/tabbar'
|
|
import { navScroll } from '../../utils/nav'
|
|
import { goLogin } from '../../utils/guest'
|
|
import { logger } from '../../utils/logger'
|
|
|
|
// 日志模块名
|
|
const LOG = 'profile'
|
|
|
|
function getZodiac(birthDay: number): string {
|
|
if (!birthDay) return ''
|
|
const d = new Date(birthDay * 1000)
|
|
const m = d.getMonth() + 1
|
|
const day = d.getDate()
|
|
const arr = [
|
|
[120, '摩羯座'], [218, '水瓶座'], [320, '双鱼座'], [419, '白羊座'],
|
|
[520, '金牛座'], [621, '双子座'], [722, '巨蟹座'], [822, '狮子座'],
|
|
[922, '处女座'], [1023, '天秤座'], [1122, '天蝎座'], [1221, '射手座'],
|
|
]
|
|
const val = m * 100 + day
|
|
for (let i = 0; i < arr.length; i++) {
|
|
if (val <= (arr[i][0] as number)) return arr[i][1] as string
|
|
}
|
|
return '摩羯座'
|
|
}
|
|
|
|
Page({
|
|
data: {
|
|
userInfo: {} as UserInfo,
|
|
// 会员标签(vip_type 2/3/4 → 月度/年度/体验会员;非会员为空不展示)
|
|
vipLabel: '',
|
|
uploadRemain: 0,
|
|
zodiac: '',
|
|
unread: 0,
|
|
editVisible: false,
|
|
avatarUploading: false,
|
|
avatarChanged: false,
|
|
saving: false,
|
|
editForm: {
|
|
username: '',
|
|
sex: 0,
|
|
birth_day: 0,
|
|
avatar: '',
|
|
},
|
|
birthDateStr: '',
|
|
today: '',
|
|
// 四栏额度条:上传 / AI 推荐 / 社区分享 / 衣柜分享
|
|
quotaStats: [] as Array<{ key: number; label: string; remain: number; total: number }>,
|
|
// 滚动收缩:由 utils/nav.ts 的共享 handler 驱动
|
|
navScrolled: false,
|
|
// 游客态(未登录):展示资料引导卡,只读入口仍可访问,不发请求
|
|
guest: false,
|
|
},
|
|
|
|
// scroll-view 滚动 → 导航收缩为玻璃小标题
|
|
onNavScroll: navScroll,
|
|
|
|
onLoad() {
|
|
this.setToday()
|
|
// 首帧渲染前判定游客态,避免首帧 setData 冲突
|
|
if (!auth.isLoggedIn()) {
|
|
this.setData({ guest: true })
|
|
logger.info(LOG, '未登录进入我的页,渲染游客态资料卡')
|
|
}
|
|
},
|
|
|
|
// 跨零点后回到本页时,生日选择器上限需重算,不能停留在昨天
|
|
setToday() {
|
|
const d = new Date()
|
|
const pad = (n: number) => (n < 10 ? `0${n}` : `${n}`)
|
|
this.setData({ today: `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}` })
|
|
},
|
|
|
|
onShow() {
|
|
this.syncTabBar()
|
|
this.setToday()
|
|
// 回到本页时按抽屉状态校准标签栏,防止切前后台/切 tab 后残留隐藏
|
|
setTabBarHidden(!!this.data.editVisible)
|
|
// 未登录:游客态展示引导卡,零请求,不再强制跳登录(微信审核合规要求)
|
|
if (!auth.isLoggedIn()) {
|
|
if (!this.data.guest) {
|
|
// 退出登录后回到本页时清掉残留资料,避免展示上一账号信息
|
|
this.setData({
|
|
guest: true,
|
|
userInfo: {} as UserInfo,
|
|
vipLabel: '',
|
|
zodiac: '',
|
|
quotaStats: [],
|
|
uploadRemain: 0,
|
|
unread: 0,
|
|
})
|
|
}
|
|
return
|
|
}
|
|
// 登录成功回到本页:清掉游客态,拉取真实资料
|
|
if (this.data.guest) {
|
|
logger.info(LOG, '检测到登录态,从游客态切回真实资料')
|
|
this.setData({ guest: false })
|
|
}
|
|
this.consumeOpenEditMark()
|
|
this.fetchUser()
|
|
this.fetchQuota()
|
|
this.fetchUnread()
|
|
},
|
|
|
|
// 未登录时的统一拦截:进入登录页,由用户自行选择是否登录
|
|
needLogin(): boolean {
|
|
if (auth.isLoggedIn()) return false
|
|
logger.debug(LOG, '游客态点击受限入口,引导登录')
|
|
goLogin()
|
|
return true
|
|
},
|
|
|
|
// 游客态点击资料卡 / 登录引导
|
|
onGuestLogin() {
|
|
this.needLogin()
|
|
},
|
|
|
|
// 离开本页时恢复标签栏:此时抽屉不再可见,标签栏不能残留隐藏
|
|
onHide() {
|
|
setTabBarHidden(false)
|
|
},
|
|
|
|
// 拉取未读数:与 tabBar 红点同源(/notification/unread),保证两处数量一致
|
|
fetchUnread() {
|
|
request<{ unread_count: number }>({ url: '/notification/unread', showError: false, silentAuth: true })
|
|
.then((res) => {
|
|
const unread = res.unread_count || 0
|
|
logger.debug(LOG, '未读数加载成功, unread=', unread)
|
|
// 写回缓存并同步 tabBar 红点(setUnread 内部只更新,不再发请求)
|
|
wx.setStorageSync(STORAGE_KEYS.UNREAD, unread)
|
|
this.setData({ unread })
|
|
setTabBarUnread(unread)
|
|
})
|
|
.catch((e: any) => logger.warn(LOG, '未读数加载失败(静默降级), code=', e && e.code))
|
|
},
|
|
|
|
// 消费「引导完善资料」标记:命中则自动弹出编辑弹窗(标记一次性,先清除再打开)
|
|
consumeOpenEditMark() {
|
|
if (!wx.getStorageSync(STORAGE_KEYS.OPEN_PROFILE_EDIT)) return
|
|
wx.removeStorageSync(STORAGE_KEYS.OPEN_PROFILE_EDIT)
|
|
logger.debug(LOG, '命中打开编辑弹窗标记,自动弹出资料编辑')
|
|
// 首帧渲染未结束就 setData 会触发「Expected updated data but get first rendering data」,
|
|
// 导致 webview 渲染失败(视觉上仍停留在上一页),故延迟到下一帧再打开
|
|
wx.nextTick(() => this.openEditFromGuide())
|
|
},
|
|
|
|
// 引导入口进入时自动打开编辑弹窗:fetchUser 未返回时用本地缓存兜底,避免表单为空
|
|
openEditFromGuide() {
|
|
if (!Object.keys(this.data.userInfo || {}).length) {
|
|
const cached = wx.getStorageSync(STORAGE_KEYS.USER_INFO)
|
|
if (cached && Object.keys(cached).length) {
|
|
logger.debug(LOG, '用户信息未就绪,使用本地缓存预填编辑表单')
|
|
this.setData({
|
|
userInfo: cached,
|
|
zodiac: getZodiac(cached.birth_day),
|
|
vipLabel: VIP_TYPE_NAMES[cached.vip_type] || '',
|
|
})
|
|
}
|
|
}
|
|
this.onEdit()
|
|
},
|
|
|
|
syncTabBar() {
|
|
// 红点由 tabBar 自身在登录态下同步,这里只更新选中态
|
|
setTabBarSelected(4)
|
|
},
|
|
|
|
fetchUser() {
|
|
request<UserInfo>({ url: '/user/info', showError: false })
|
|
.then((user) => {
|
|
logger.debug(LOG, '用户信息加载成功, vipType=', user.vip_type)
|
|
wx.setStorageSync(STORAGE_KEYS.USER_INFO, user)
|
|
// 会员标签按 vip_type 判定:2 月度 / 3 年度 / 4 体验(新用户默认赠送),非会员不展示
|
|
this.setData({
|
|
userInfo: user,
|
|
zodiac: getZodiac(user.birth_day),
|
|
vipLabel: VIP_TYPE_NAMES[user.vip_type] || '',
|
|
})
|
|
})
|
|
.catch((e: any) => logger.warn(LOG, '用户信息加载失败, code=', e && e.code))
|
|
},
|
|
|
|
fetchQuota() {
|
|
request<{ quotas: QuotaItem[] }>({ url: '/quota/info', showError: false, silentAuth: true })
|
|
.then((res) => {
|
|
const quotas = res && Array.isArray(res.quotas) ? res.quotas : []
|
|
const upload = quotas.find((q) => q.quota_type === QuotaType.UPLOAD)
|
|
if (upload) this.setData({ uploadRemain: upload.remain })
|
|
// 四栏额度条按固定顺序取数,缺失类型补 0,保证栏位不塌陷
|
|
const order = [QuotaType.UPLOAD, QuotaType.AI, QuotaType.OUTFIT_SHARE, QuotaType.CLOSET_SHARE]
|
|
const quotaStats = order.map((type) => {
|
|
const hit = quotas.find((q) => q.quota_type === type)
|
|
return {
|
|
key: type,
|
|
label: QUOTA_NAMES[type] || '额度',
|
|
remain: hit ? hit.remain : 0,
|
|
total: hit ? hit.total : 0,
|
|
}
|
|
})
|
|
this.setData({ quotaStats })
|
|
logger.debug(LOG, '额度加载成功, 上传剩余=', upload ? upload.remain : 0, '栏位=', quotaStats.length)
|
|
})
|
|
.catch((e: any) => logger.warn(LOG, '额度加载失败(静默降级), code=', e && e.code))
|
|
},
|
|
|
|
onEdit() {
|
|
if (this.needLogin()) return
|
|
const u = this.data.userInfo
|
|
// 抽屉打开期间隐藏悬浮标签栏:tabBar 由框架渲染在页面之上,否则会压住抽屉底部按钮
|
|
setTabBarHidden(true)
|
|
this.setData({
|
|
editVisible: true,
|
|
avatarChanged: false,
|
|
editForm: {
|
|
username: u.username || '',
|
|
sex: u.sex || 0,
|
|
birth_day: u.birth_day || 0,
|
|
avatar: u.avatar || '',
|
|
},
|
|
birthDateStr: u.birth_day ? formatDate(u.birth_day) : '',
|
|
})
|
|
},
|
|
|
|
onCloseEdit() {
|
|
this.setData({ editVisible: false })
|
|
setTabBarHidden(false)
|
|
},
|
|
|
|
noop() {},
|
|
|
|
onChooseAvatar(e: WechatMiniprogram.CustomEvent) {
|
|
this.setData({ 'editForm.avatar': e.detail.avatarUrl, avatarChanged: true })
|
|
},
|
|
|
|
onNameInput(e: WechatMiniprogram.Input) {
|
|
this.setData({ 'editForm.username': e.detail.value })
|
|
},
|
|
|
|
// 性别为两选一字段:抽屉内联胶囊直接选中,不再弹动作表(避免两层玻璃叠加)
|
|
onSexTap(e: WechatMiniprogram.TouchEvent) {
|
|
const sex = Number((e.currentTarget.dataset as any).value) || 0
|
|
logger.debug(LOG, '选择性别, sex=', sex)
|
|
this.setData({ 'editForm.sex': sex })
|
|
},
|
|
|
|
onBirthChange(e: WechatMiniprogram.CustomEvent) {
|
|
const dateStr = e.detail.value as string
|
|
const ts = Math.floor(new Date(dateStr.replace(/-/g, '/')).getTime() / 1000)
|
|
this.setData({ 'editForm.birth_day': ts, birthDateStr: dateStr })
|
|
},
|
|
|
|
onSaveProfile() {
|
|
if (this.data.avatarUploading || this.data.saving) return
|
|
const f = this.data.editForm
|
|
const avatar = f.avatar || ''
|
|
const username = (f.username || '').trim()
|
|
if (!username) {
|
|
wx.showToast({ title: '昵称不能为空', icon: 'none' })
|
|
return
|
|
}
|
|
|
|
const finish = () => {
|
|
this.setData({
|
|
editVisible: false,
|
|
avatarUploading: false,
|
|
saving: false,
|
|
avatarChanged: false,
|
|
})
|
|
// 抽屉关闭,恢复悬浮标签栏
|
|
setTabBarHidden(false)
|
|
this.fetchUser()
|
|
}
|
|
|
|
const saveProfile = () => {
|
|
const u = this.data.userInfo
|
|
const body: Record<string, any> = {}
|
|
// 只提交真正变更的字段,避免无意义请求与误清空
|
|
if (username !== (u.username || '')) body.username = username
|
|
if (f.sex && f.sex !== u.sex) body.sex = f.sex
|
|
if (f.birth_day && f.birth_day !== u.birth_day) body.birth_day = f.birth_day
|
|
|
|
if (!Object.keys(body).length && !this.data.avatarChanged) {
|
|
// 无变更直接关闭:同样要恢复标签栏,否则会残留隐藏
|
|
this.setData({ editVisible: false })
|
|
setTabBarHidden(false)
|
|
return
|
|
}
|
|
|
|
this.setData({ saving: true })
|
|
logger.debug(LOG, '提交资料变更, 字段=', Object.keys(body).join(','))
|
|
request({ url: '/user/profile', method: 'PUT', data: body })
|
|
.then(() => {
|
|
logger.info(LOG, '资料保存成功')
|
|
wx.showToast({ title: '已保存', icon: 'success' })
|
|
finish()
|
|
})
|
|
.catch((e: any) => {
|
|
logger.warn(LOG, '资料保存失败, code=', e && e.code)
|
|
this.setData({ avatarUploading: false, saving: false })
|
|
})
|
|
}
|
|
|
|
if (this.data.avatarChanged && avatar) {
|
|
this.setData({ avatarUploading: true })
|
|
uploadFile<{ avatar: string }>(avatar, 'file', '/user/avatar/upload')
|
|
.then(() => {
|
|
logger.info(LOG, '头像上传成功')
|
|
saveProfile()
|
|
})
|
|
.catch((e: any) => {
|
|
logger.warn(LOG, '头像上传失败, code=', e && e.code)
|
|
this.setData({ avatarUploading: false, saving: false })
|
|
})
|
|
} else {
|
|
saveProfile()
|
|
}
|
|
},
|
|
|
|
onTapMall() {
|
|
if (this.needLogin()) return
|
|
wx.navigateTo({
|
|
url: '/pages/mall/mall',
|
|
fail: (e) => logger.warn(LOG, 'navigateTo 商城失败, err=', e && e.errMsg),
|
|
})
|
|
},
|
|
onTapOrder() {
|
|
if (this.needLogin()) return
|
|
wx.navigateTo({
|
|
url: '/pages/order/order',
|
|
fail: (e) => logger.warn(LOG, 'navigateTo 订单失败, err=', e && e.errMsg),
|
|
})
|
|
},
|
|
onTapNotification() {
|
|
if (this.needLogin()) return
|
|
wx.navigateTo({
|
|
url: '/pages/notification/notification',
|
|
fail: (e) => logger.warn(LOG, 'navigateTo 通知失败, err=', e && e.errMsg),
|
|
})
|
|
},
|
|
onTapCollect() {
|
|
if (this.needLogin()) return
|
|
wx.setStorageSync(STORAGE_KEYS.OPEN_COLLECT, 1)
|
|
wx.switchTab({
|
|
url: '/pages/outfit/outfit',
|
|
fail: (e) => logger.warn(LOG, 'switchTab 穿搭失败, err=', e && e.errMsg),
|
|
})
|
|
},
|
|
onTapFlow() {
|
|
if (this.needLogin()) return
|
|
wx.navigateTo({
|
|
url: '/pages/mall/mall?tab=flow',
|
|
fail: (e) => logger.warn(LOG, 'navigateTo 流水失败, err=', e && e.errMsg),
|
|
})
|
|
},
|
|
onTapService() {
|
|
wx.showToast({ title: '客服与反馈敬请期待', icon: 'none' })
|
|
},
|
|
onTapAbout() {
|
|
wx.showToast({ title: '纸条AI衣橱 v1.0.0', icon: 'none' })
|
|
},
|
|
// 协议入口常驻设置分组,随时可查阅(协议页无网络请求,登录态异常也可打开)
|
|
onTapUserAgreement() {
|
|
this.openAgreement('user')
|
|
},
|
|
onTapPrivacy() {
|
|
this.openAgreement('privacy')
|
|
},
|
|
openAgreement(type: 'user' | 'privacy') {
|
|
logger.debug(LOG, '打开协议, type=', type)
|
|
wx.navigateTo({
|
|
url: `/pages/agreement/agreement?type=${type}`,
|
|
fail: (e) => logger.warn(LOG, 'navigateTo 协议页失败, err=', e && e.errMsg),
|
|
})
|
|
},
|
|
})
|