204 lines
7.1 KiB
TypeScript
204 lines
7.1 KiB
TypeScript
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<CommunityOutfitItem> & {
|
|
_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<void> {
|
|
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<PagedResult<OutfitRow>>({ url, data: { page, page_size: this.data.pageSize } })
|
|
.then(async (res) => {
|
|
const { list: raw, total } = normalizePaged<OutfitRow>(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<OutfitRow[]> {
|
|
// 社区分段:作者与互动计数由后端一次性下发,本地仅补相对时间,避免逐条请求点赞接口
|
|
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),
|
|
})
|
|
},
|
|
})
|