import { request, redirectToLogin } from '../../utils/request' import * as auth from '../../utils/auth' import { PagedResult, OutfitItem, CommunityOutfitItem } from '../../types/models' import { OutfitSource, IsPublic, STORAGE_KEYS } from '../../utils/config' import { normalizePaged, mergePaged, hasMorePaged } from '../../utils/paged' import { setTabBarSelected } from '../../utils/tabbar' import { formatRelativeTime } from '../../utils/format' import { navScroll } from '../../utils/nav' import { logger } from '../../utils/logger' // 日志模块名 const LOG = 'outfit' // 拼图封面单元:无图时前端渲染线稿占位 type CoverCell = { k: string; img: string } // 列表行:我的穿搭/已收藏来自 OutfitItem;社区来自 CommunityOutfitItem(额外带作者与互动计数)。 // 统一为交叉类型,模板按 activeTab 条件渲染差异字段。 type OutfitRow = OutfitItem & Partial & { _likeCount?: number _collectCount?: number // cls 为胶囊类名(零彩色:pill--ink / pill--line / 空) _status?: { text: string; cls: string } _time?: string _covers?: CoverCell[] _collageCls?: string _moreCount?: number } Page({ data: { activeTab: 0, list: [] as OutfitRow[], loading: true, refreshing: false, loadingMore: false, page: 1, pageSize: 20, hasMore: true, // 滚动收缩:由 utils/nav.ts 的共享 handler 驱动 navScrolled: false, }, // scroll-view 滚动 → 导航收缩为玻璃小标题 onNavScroll: navScroll, onShow() { this.syncTabBar() // 未登录直接回登录页(幂等跳转),不再发出必然 401 的请求 if (!auth.isLoggedIn()) { redirectToLogin() return } const openCollect = wx.getStorageSync(STORAGE_KEYS.OPEN_COLLECT) if (openCollect) { logger.debug(LOG, '命中打开收藏夹标记,切换到收藏 tab') wx.removeStorageSync(STORAGE_KEYS.OPEN_COLLECT) if (this.data.activeTab !== 1) { this.setData({ activeTab: 1, list: [], loading: true }) } } // 首帧渲染未结束就 setData 会触发「Expected updated data but get first rendering data」, // 整个 webview 渲染失败,用户视觉上仍看到上一个页面。首次 onShow 的刷新延迟到下一帧 if (!(this as any).firstShown) { ;(this as any).firstShown = true wx.nextTick(() => this.refresh()) return } this.refresh() }, syncTabBar() { setTabBarSelected(1) }, onTabChange(e: WechatMiniprogram.TouchEvent) { const index = Number(e.currentTarget.dataset.index) if (index === this.data.activeTab) return this.setData({ activeTab: index, list: [], loading: true }) this.refresh() }, refresh() { this.setData({ page: 1, hasMore: true, loading: this.data.list.length === 0 }) this.fetchList(1) }, onRefresh() { this.setData({ refreshing: true }) this.fetchList(1).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 tab = this.data.activeTab const url = tab === 1 ? '/collect' : tab === 2 ? '/outfit/community' : '/outfit' const seq = ((this as any).reqSeq || 0) + 1 ;(this as any).reqSeq = seq if (page > 1) this.setData({ loadingMore: true }) return request>({ url, data: { page, page_size: this.data.pageSize } }) .then(async (res) => { const { list: raw, total } = normalizePaged(res) logger.debug(LOG, '列表加载成功, url=', url, 'page=', page, '条数=', raw && raw.length) const items = await this.decorateList(raw) // decorateList 是异步的,期间可能已切换 tab:序号不匹配则丢弃 if ((this as any).reqSeq !== seq) return const merged = mergePaged(this.data.list, items, page === 1) this.setData({ list: merged, 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, '列表加载失败, url=', url, 'page=', page, 'code=', e && e.code) this.setData({ loading: false, loadingMore: false }) }) }, decorateList(list: OutfitRow[]): Promise { // 社区分段:作者与互动计数由后端一次性下发,本地仅补相对时间,避免逐条请求点赞接口 if (this.data.activeTab === 2) { return Promise.resolve( list.map((it) => ({ ...it, _likeCount: it.like_count || 0, _collectCount: it.collect_count || 0, _time: formatRelativeTime(it.created_at), })), ) } return Promise.all( list.map(async (it) => { let likeCount = 0 try { const like = await request<{ is_liked: boolean; like_count: number }>({ url: `/like/${it.outfit_uuid}`, showError: false, }) likeCount = like.like_count } catch (e: any) { logger.warn(LOG, '点赞数拉取失败, uuid=', it.outfit_uuid, 'code=', e && e.code) } return { ...it, ...this.decorateCover(it), _likeCount: likeCount, _status: this.getStatusText(it) } }) ) }, // 无缝拼图封面:取前 4 件,超出用 +N 角标;不足 1 件时补一个线稿占位格 decorateCover(it: OutfitItem): { _covers: CoverCell[]; _collageCls: string; _moreCount: number } { const items = it.items || [] const covers: CoverCell[] = items.slice(0, 4).map((c) => ({ k: c.clothes_uuid, img: c.processed_image || '', })) if (!covers.length) covers.push({ k: 'empty', img: '' }) return { _covers: covers, _collageCls: `collage--${covers.length}`, _moreCount: Math.max(items.length - 4, 0), } }, // 零彩色状态语义:AI 来源 = 黑底白字,已公开 = 描边,仅自己可见 = 浅灰 getStatusText(it: OutfitItem): { text: string; cls: string } { if (it.source === OutfitSource.AI) return { text: 'AI 推荐', cls: 'pill--ink' } if (it.is_public === IsPublic.PUBLIC) return { text: '已公开', cls: 'pill--line' } return { text: '仅自己可见', cls: '' } }, onCreate() { wx.navigateTo({ url: '/pages/outfit-edit/outfit-edit', fail: (e) => logger.warn(LOG, 'navigateTo 新建穿搭失败, err=', e && e.errMsg), }) }, onAiRecommend() { wx.navigateTo({ url: '/pages/ai-recommend/ai-recommend', fail: (e) => logger.warn(LOG, 'navigateTo AI 推荐失败, err=', e && e.errMsg), }) }, onOutfitTap(e: WechatMiniprogram.TouchEvent) { const uuid = e.currentTarget.dataset.uuid as string const owner = this.data.activeTab === 0 ? '1' : '0' logger.debug(LOG, '点击穿搭, uuid=', uuid, 'owner=', owner) wx.navigateTo({ url: `/pages/outfit-detail/outfit-detail?uuid=${uuid}&owner=${owner}`, fail: (err) => logger.warn(LOG, 'navigateTo 穿搭详情失败, err=', err && err.errMsg), }) }, })