Files
zt-closet-wxmini-frontend/miniprogram/pages/profile/profile.ts
T
2026-09-15 17:18:15 +08:00

300 lines
10 KiB
TypeScript

import { request, uploadFile, redirectToLogin } from '../../utils/request'
import * as auth from '../../utils/auth'
import { UserInfo, QuotaItem } from '../../types/models'
import { QuotaType, STORAGE_KEYS } from '../../utils/config'
import { formatDate } from '../../utils/format'
import { setTabBarSelected, setTabBarUnread, setTabBarHidden } from '../../utils/tabbar'
import { navScroll } from '../../utils/nav'
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,
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,
},
// scroll-view 滚动 → 导航收缩为玻璃小标题
onNavScroll: navScroll,
onLoad() {
this.setToday()
},
// 跨零点后回到本页时,生日选择器上限需重算,不能停留在昨天
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)
// 未登录直接回登录页(幂等跳转),不再发出必然 401 的请求
if (!auth.isLoggedIn()) {
redirectToLogin()
return
}
this.consumeOpenEditMark()
this.fetchUser()
this.fetchQuota()
this.fetchUnread()
},
// 离开本页时恢复标签栏:此时抽屉不再可见,标签栏不能残留隐藏
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) })
}
}
this.onEdit()
},
syncTabBar() {
// 红点由 tabBar 自身在登录态下同步,这里只更新选中态
setTabBarSelected(4)
},
fetchUser() {
request<UserInfo>({ url: '/user/info', showError: false })
.then((user) => {
logger.debug(LOG, '用户信息加载成功')
wx.setStorageSync(STORAGE_KEYS.USER_INFO, user)
this.setData({ userInfo: user, zodiac: getZodiac(user.birth_day) })
})
.catch((e: any) => logger.warn(LOG, '用户信息加载失败, code=', e && e.code))
},
fetchQuota() {
request<{ quotas: QuotaItem[] }>({ url: '/quota/info', showError: false, silentAuth: true })
.then((res) => {
const upload = res.quotas.find((q) => q.quota_type === QuotaType.UPLOAD)
if (upload) this.setData({ uploadRemain: upload.remain })
logger.debug(LOG, '额度加载成功, 上传剩余=', upload ? upload.remain : 0)
})
.catch((e: any) => logger.warn(LOG, '额度加载失败(静默降级), code=', e && e.code))
},
onEdit() {
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() {
wx.navigateTo({
url: '/pages/mall/mall',
fail: (e) => logger.warn(LOG, 'navigateTo 商城失败, err=', e && e.errMsg),
})
},
onTapOrder() {
wx.navigateTo({
url: '/pages/order/order',
fail: (e) => logger.warn(LOG, 'navigateTo 订单失败, err=', e && e.errMsg),
})
},
onTapNotification() {
wx.navigateTo({
url: '/pages/notification/notification',
fail: (e) => logger.warn(LOG, 'navigateTo 通知失败, err=', e && e.errMsg),
})
},
onTapCollect() {
wx.setStorageSync(STORAGE_KEYS.OPEN_COLLECT, 1)
wx.switchTab({
url: '/pages/outfit/outfit',
fail: (e) => logger.warn(LOG, 'switchTab 穿搭失败, err=', e && e.errMsg),
})
},
onTapFlow() {
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' })
},
})