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 } 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, // 性别选择:v2 玻璃动作表 sexVisible: false, sexItems: [ { text: '男', value: 1 }, { text: '女', value: 2 }, ], }, // 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() // 未登录直接回登录页(幂等跳转),不再发出必然 401 的请求 if (!auth.isLoggedIn()) { redirectToLogin() return } this.consumeOpenEditMark() this.fetchUser() this.fetchQuota() this.fetchUnread() }, // 拉取未读数:与 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({ 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 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 }) }, 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 }) }, onSexSelect() { this.setData({ sexVisible: true }) }, onSexPick(e: WechatMiniprogram.CustomEvent) { const sex = Number(e.detail.value) || 0 logger.debug(LOG, '选择性别, sex=', sex) this.setData({ 'editForm.sex': sex, sexVisible: false }) }, onSexCancel() { this.setData({ sexVisible: false }) }, 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, }) this.fetchUser() } const saveProfile = () => { const u = this.data.userInfo const body: Record = {} // 只提交真正变更的字段,避免无意义请求与误清空 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 }) 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' }) }, })