import { request, redirectToLogin } from '../../utils/request' import * as auth from '../../utils/auth' import { PagedResult, ClothesItem, QuotaItem } from '../../types/models' import { QuotaType } from '../../utils/config' import { normalizePaged, mergePaged, hasMorePaged } from '../../utils/paged' import { clothesStatusFlag } from '../../utils/clothes-status' import { resolvePendingClothes } from '../../utils/pending-clothes' import { setTabBarSelected } from '../../utils/tabbar' import { navScroll } from '../../utils/nav' import { logger } from '../../utils/logger' // 日志模块名 const LOG = 'wardrobe' const CATEGORIES = [ { name: '全部', value: '' }, { name: '上装', value: '上装' }, { name: '下装', value: '下装' }, { name: '连衣裙', value: '连衣裙' }, { name: '外套', value: '外套' }, { name: '鞋', value: '鞋' }, { name: '配饰', value: '配饰' }, ] Page({ data: { categories: CATEGORIES, activeCategory: 0, list: [] as ClothesItem[], total: 0, uploadRemain: 0, loading: true, refreshing: false, loadingMore: false, // 上拉加载独立闸门,避免与首次 loading 互相干扰 page: 1, pageSize: 20, hasMore: true, // 滚动收缩:由 utils/nav.ts 的共享 handler 驱动 navScrolled: false, // 「上传后退出小程序」的失败结果回归告知(v2 玻璃弹窗) failVisible: false, failText: '', failUuid: '', failButtons: [ { text: '知道了', value: '' }, { text: '查看衣物', value: 'ok', type: 'main' }, ], }, // scroll-view 滚动 → 导航收缩为玻璃小标题 onNavScroll: navScroll, onShow() { this.syncTabBar() // 未登录直接回登录页(幂等跳转),不再发出必然 401 的请求 if (!auth.isLoggedIn()) { redirectToLogin() return } // 核对「上传后退出小程序」的衣物最终处理结果(pending 为空时零请求) this.resolvePending() // 首帧渲染未结束就 setData 会触发「Expected updated data but get first rendering data」, // 整个 webview 渲染失败,用户视觉上仍看到上一个页面(从 login switchTab 过来时表现为 // "卡在登录页")。把首次 onShow 的刷新延迟到下一帧,规避首帧冲突。 if (!(this as any).firstShown) { ;(this as any).firstShown = true wx.nextTick(() => this.refresh()) return } logger.debug(LOG, 'onShow 触发刷新') this.refresh() }, syncTabBar() { setTabBarSelected(0) }, refresh() { this.setData({ page: 1, hasMore: true, loading: this.data.list.length === 0 }) this.fetchList(1) this.fetchQuota() }, onRefresh() { this.setData({ refreshing: true }) Promise.all([this.fetchList(1), this.fetchQuota()]).finally(() => { this.setData({ refreshing: false }) }) }, onReachBottom() { if (!this.data.hasMore || this.data.loading || this.data.loadingMore) return this.fetchList(this.data.page + 1) }, fetchList(page: number): Promise { const cat = CATEGORIES[this.data.activeCategory].value const params: Record = { page, page_size: this.data.pageSize } if (cat) params.category_main = cat // 请求序号:快速切分类/下拉刷新时旧响应可能后到,序号不匹配的响应直接丢弃 const seq = ((this as any).reqSeq || 0) + 1 ;(this as any).reqSeq = seq if (page > 1) this.setData({ loadingMore: true }) return request>({ url: '/clothes', data: params }) .then((res) => { if ((this as any).reqSeq !== seq) return const { list: raw, total } = normalizePaged(res) logger.debug(LOG, '列表加载成功, 分类=', cat || '全部', 'page=', page, '条数=', raw && raw.length) // 状态标识收敛到 clothes-status:处理中(玻璃)/ 处理失败(实心)/ 无 const list = raw.map((it) => { const flag = clothesStatusFlag(it) return { ...it, _statusText: flag.text, _statusType: flag.type } }) const merged = mergePaged(this.data.list, list, page === 1) this.setData({ list: merged, total, hasMore: hasMorePaged(merged, total, this.data.pageSize), loading: false, loadingMore: false, page, }) }) .catch((e: any) => { if ((this as any).reqSeq !== seq) return logger.warn(LOG, '列表加载失败, page=', page, 'code=', e && e.code) this.setData({ loading: false, loadingMore: false }) }) }, fetchQuota(): Promise { return 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)) }, // 核对待处理衣物:成功静默、失败弹窗告知;有终结态时重刷列表对齐状态 resolvePending() { if ((this as any).resolvingPending) return ;(this as any).resolvingPending = true resolvePendingClothes() .then((res) => { if (!res.finished.length && !res.failed.length) return logger.info(LOG, '待核对衣物已终结, 成功=', res.finished.length, '失败=', res.failed.length) if (res.failed.length) this.showFailModal(res.failed) this.refresh() }) .catch((e: any) => logger.warn(LOG, '待核对衣物解析异常, code=', e && e.code)) .finally(() => { ;(this as any).resolvingPending = false }) }, showFailModal(failed: Array<{ uuid: string; reason: string }>) { const first = (failed[0] || ({} as { uuid: string; reason: string })) as { uuid: string reason: string } const content = failed.length === 1 ? `1 件衣物识别失败:${first.reason},额度已自动退回` : `${failed.length} 件衣物识别失败,额度已自动退回,可进入详情查看原因` this.setData({ failVisible: true, failText: content, failUuid: first.uuid || '' }) }, onFailCancel() { this.setData({ failVisible: false }) }, onFailConfirm(e: WechatMiniprogram.CustomEvent) { const uuid = this.data.failUuid this.setData({ failVisible: false }) if (e.detail.value !== 'ok' || !uuid) return wx.navigateTo({ url: `/pages/clothes-detail/clothes-detail?uuid=${uuid}`, fail: (err) => logger.warn(LOG, 'navigateTo 衣物详情失败, err=', err && err.errMsg), }) }, onCategoryTap(e: WechatMiniprogram.TouchEvent) { const index = Number(e.currentTarget.dataset.index) if (index === this.data.activeCategory) return this.setData({ activeCategory: index, list: [], loading: true }) this.refresh() }, onClothesTap(e: WechatMiniprogram.CustomEvent) { const item = e.detail.item as ClothesItem logger.debug(LOG, '点击衣物, uuid=', item && item.clothes_uuid) wx.navigateTo({ url: `/pages/clothes-detail/clothes-detail?uuid=${item.clothes_uuid}`, fail: (err) => logger.warn(LOG, 'navigateTo 衣物详情失败, err=', err && err.errMsg), }) }, onUpload() { wx.navigateTo({ url: '/pages/upload/upload', fail: (e) => logger.warn(LOG, 'navigateTo 上传页失败, err=', e && e.errMsg), }) }, })